From f494061533584ad741feeb4f4fe674e154eaee97 Mon Sep 17 00:00:00 2001 From: leogdion Date: Sun, 4 Feb 2024 11:18:46 -0500 Subject: [PATCH] v2.0.0-alpha.1 (#18) --- .github/workflows/Sublimation.yml | 117 ++- .github/workflows/codeql.yml | 26 +- .periphery.yml | 1 - .strict.stringslint.yml | 17 - .stringslint.yml | 17 - .swiftformat | 6 +- .swiftlint.yml | 26 +- .../contents.xcworkspacedata | 7 - .../xcshareddata/swiftpm/Package.resolved | 153 ++-- Demo/SublimationDemoApp/ContentView.swift | 8 +- Demo/SublimationDemoServer/Package.swift | 4 +- Mintfile | 6 +- Package.resolved | 140 +-- Package.swift | 112 ++- Sources/NgrokOpenAPIClient/Client.swift | 305 +++++++ Sources/NgrokOpenAPIClient/Types.swift | 846 ++++++++++++++++++ Sources/Ngrokit/FileHandle.swift | 69 +- Sources/Ngrokit/ListTunnelsRequest.swift | 26 - Sources/Ngrokit/Ngrok.swift | 73 -- Sources/Ngrokit/NgrokCLIAPI.swift | 54 ++ Sources/Ngrokit/NgrokClient.swift | 120 +++ Sources/Ngrokit/NgrokError.swift | 108 +++ Sources/Ngrokit/NgrokMacProcess.swift | 115 +++ Sources/Ngrokit/NgrokProcess.swift | 47 + Sources/Ngrokit/NgrokProcessCLIAPI.swift | 67 ++ Sources/Ngrokit/NgrokTunnel.swift | 13 - .../Ngrokit/NgrokTunnelConfiguration.swift | 46 +- Sources/Ngrokit/NgrokTunnelRequest.swift | 17 - Sources/Ngrokit/NgrokTunnelResponse.swift | 4 - Sources/Ngrokit/NgrokUrlParser.swift | 17 - Sources/Ngrokit/Pipable.swift | 46 + Sources/Ngrokit/Processable.swift | 103 +++ Sources/Ngrokit/ProcessableProcess.swift | 111 +++ Sources/Ngrokit/RuntimeError.swift | 38 + Sources/Ngrokit/StartTunnelRequest.swift | 32 - Sources/Ngrokit/StopTunnelRequest.swift | 34 - Sources/Ngrokit/TerminationReason.swift | 47 + Sources/Ngrokit/Tunnel.swift | 106 +++ Sources/Ngrokit/TunnelRequest.swift | 74 ++ Sources/NgrokitMocks/MockAPI.swift | 92 ++ Sources/NgrokitMocks/MockDataHandle.swift | 61 ++ Sources/NgrokitMocks/MockNgrokCLIAPI.swift | 48 + Sources/NgrokitMocks/MockNgrokProcess.swift | 41 + Sources/NgrokitMocks/MockPipe.swift | 41 + Sources/NgrokitMocks/MockProcess.swift | 92 ++ Sources/NgrokitMocks/URL.swift | 40 + Sources/Sublimation/AnyKVdbTunnelClient.swift | 47 - Sources/Sublimation/KVdb.swift | 82 +- Sources/Sublimation/KVdbTunnelClient.swift | 70 +- .../Sublimation/KVdbTunnelRepository.swift | 65 +- .../KVdbTunnelRepositoryFactory.swift | 63 ++ .../Sublimation/KVdbURLConstructable.swift | 52 ++ Sources/Sublimation/NgrokServerError.swift | 37 + Sources/Sublimation/Optional.swift | 43 + Sources/Sublimation/Result.swift | 42 + Sources/Sublimation/TunnelRepository.swift | 54 +- .../Sublimation/TunnelRepositoryFactory.swift | 69 ++ Sources/Sublimation/URL.swift | 47 + Sources/Sublimation/URLSession.swift | 87 ++ Sources/Sublimation/URLSessionClient.swift | 120 +-- .../WritableTunnelRepository.swift | 58 +- .../WritableTunnelRepositoryFactory.swift | 47 + Sources/SublimationMocks/MockError.swift | 52 ++ .../SublimationMocks/MockTunnelClient.swift | 76 ++ Sources/SublimationMocks/MockURL.swift | 39 + Sources/SublimationMocks/URL.swift | 40 + Sources/SublimationVapor/NetworkResult.swift | 97 ++ .../NgrokCLIAPIConfiguration.swift | 71 ++ .../SublimationVapor/NgrokCLIAPIServer.swift | 351 ++++---- .../NgrokCLIAPIServerFactory.swift | 104 +++ Sources/SublimationVapor/NgrokServer.swift | 60 +- .../NgrokServerConfiguration.swift | 36 + .../NgrokServerDelegate.swift | 52 +- .../SublimationVapor/NgrokServerError.swift | 50 ++ .../SublimationVapor/NgrokServerFactory.swift | 46 + .../NgrokVaporConfiguration.swift | 56 ++ .../SublimationVapor/ServerApplication.swift | 47 + .../SublimationLifecycleHandler.swift | 269 +++++- Sources/SublimationVapor/URI.swift | 41 + .../SublimationVapor/VaporTunnelClient.swift | 97 +- Tests/NgrokitTests/DataHandleTests.swift | 40 + Tests/NgrokitTests/NgrokClientTests.swift | 130 +++ Tests/NgrokitTests/NgrokErrorTests.swift | 95 ++ Tests/NgrokitTests/NgrokMacProcessTests.swift | 91 ++ .../NgrokProcessCLIAPITests.swift | 53 ++ Tests/SublimationTests/KVdbTests.swift | 61 ++ .../KVdbTunnelRepositoryFactoryTests.swift | 69 ++ Tests/SublimationTests/OptionalTests.swift | 47 + Tests/SublimationTests/ResultTests.swift | 49 + Tests/SublimationTests/URLTests.swift | 39 + .../MockServerApplication.swift | 37 + .../MockServerDelegate.swift | 47 + .../NetworkResultTests.swift | 216 +++++ .../NgrokCLIAPIConfigurationTests.swift | 45 + .../NgrokCLIAPIServerFactoryTests.swift | 68 ++ generate.sh | 6 + openapi-generator-config.yaml | 4 + openapi.yaml | 215 +++++ project.yml | 3 + scripts/generate.sh | 3 + scripts/lint.sh | 4 - 101 files changed, 6557 insertions(+), 905 deletions(-) delete mode 100644 .periphery.yml delete mode 100644 .strict.stringslint.yml delete mode 100644 .stringslint.yml delete mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 Sources/NgrokOpenAPIClient/Client.swift create mode 100644 Sources/NgrokOpenAPIClient/Types.swift delete mode 100644 Sources/Ngrokit/ListTunnelsRequest.swift delete mode 100644 Sources/Ngrokit/Ngrok.swift create mode 100644 Sources/Ngrokit/NgrokCLIAPI.swift create mode 100644 Sources/Ngrokit/NgrokClient.swift create mode 100644 Sources/Ngrokit/NgrokError.swift create mode 100644 Sources/Ngrokit/NgrokMacProcess.swift create mode 100644 Sources/Ngrokit/NgrokProcess.swift create mode 100644 Sources/Ngrokit/NgrokProcessCLIAPI.swift delete mode 100644 Sources/Ngrokit/NgrokTunnel.swift delete mode 100644 Sources/Ngrokit/NgrokTunnelRequest.swift delete mode 100644 Sources/Ngrokit/NgrokTunnelResponse.swift delete mode 100644 Sources/Ngrokit/NgrokUrlParser.swift create mode 100644 Sources/Ngrokit/Pipable.swift create mode 100644 Sources/Ngrokit/Processable.swift create mode 100644 Sources/Ngrokit/ProcessableProcess.swift create mode 100644 Sources/Ngrokit/RuntimeError.swift delete mode 100644 Sources/Ngrokit/StartTunnelRequest.swift delete mode 100644 Sources/Ngrokit/StopTunnelRequest.swift create mode 100644 Sources/Ngrokit/TerminationReason.swift create mode 100644 Sources/Ngrokit/Tunnel.swift create mode 100644 Sources/Ngrokit/TunnelRequest.swift create mode 100644 Sources/NgrokitMocks/MockAPI.swift create mode 100644 Sources/NgrokitMocks/MockDataHandle.swift create mode 100644 Sources/NgrokitMocks/MockNgrokCLIAPI.swift create mode 100644 Sources/NgrokitMocks/MockNgrokProcess.swift create mode 100644 Sources/NgrokitMocks/MockPipe.swift create mode 100644 Sources/NgrokitMocks/MockProcess.swift create mode 100644 Sources/NgrokitMocks/URL.swift delete mode 100644 Sources/Sublimation/AnyKVdbTunnelClient.swift create mode 100644 Sources/Sublimation/KVdbTunnelRepositoryFactory.swift create mode 100644 Sources/Sublimation/Optional.swift create mode 100644 Sources/Sublimation/Result.swift create mode 100644 Sources/Sublimation/TunnelRepositoryFactory.swift create mode 100644 Sources/Sublimation/URLSession.swift create mode 100644 Sources/Sublimation/WritableTunnelRepositoryFactory.swift create mode 100644 Sources/SublimationMocks/MockError.swift create mode 100644 Sources/SublimationMocks/MockTunnelClient.swift create mode 100644 Sources/SublimationMocks/MockURL.swift create mode 100644 Sources/SublimationMocks/URL.swift create mode 100644 Sources/SublimationVapor/NetworkResult.swift create mode 100644 Sources/SublimationVapor/NgrokCLIAPIConfiguration.swift create mode 100644 Sources/SublimationVapor/NgrokCLIAPIServerFactory.swift create mode 100644 Sources/SublimationVapor/NgrokServerConfiguration.swift create mode 100644 Sources/SublimationVapor/NgrokServerError.swift create mode 100644 Sources/SublimationVapor/NgrokServerFactory.swift create mode 100644 Sources/SublimationVapor/NgrokVaporConfiguration.swift create mode 100644 Sources/SublimationVapor/ServerApplication.swift create mode 100644 Tests/NgrokitTests/DataHandleTests.swift create mode 100644 Tests/NgrokitTests/NgrokClientTests.swift create mode 100644 Tests/NgrokitTests/NgrokErrorTests.swift create mode 100644 Tests/NgrokitTests/NgrokMacProcessTests.swift create mode 100644 Tests/NgrokitTests/NgrokProcessCLIAPITests.swift create mode 100644 Tests/SublimationTests/KVdbTests.swift create mode 100644 Tests/SublimationTests/KVdbTunnelRepositoryFactoryTests.swift create mode 100644 Tests/SublimationTests/OptionalTests.swift create mode 100644 Tests/SublimationTests/ResultTests.swift create mode 100644 Tests/SublimationTests/URLTests.swift create mode 100644 Tests/SublimationVaporTests/MockServerApplication.swift create mode 100644 Tests/SublimationVaporTests/MockServerDelegate.swift create mode 100644 Tests/SublimationVaporTests/NetworkResultTests.swift create mode 100644 Tests/SublimationVaporTests/NgrokCLIAPIConfigurationTests.swift create mode 100644 Tests/SublimationVaporTests/NgrokCLIAPIServerFactoryTests.swift create mode 100755 generate.sh create mode 100644 openapi-generator-config.yaml create mode 100644 openapi.yaml create mode 100755 scripts/generate.sh diff --git a/.github/workflows/Sublimation.yml b/.github/workflows/Sublimation.yml index ef292bd..0ee6393 100644 --- a/.github/workflows/Sublimation.yml +++ b/.github/workflows/Sublimation.yml @@ -13,9 +13,9 @@ jobs: strategy: matrix: runs-on: [ubuntu-20.04, ubuntu-22.04] - swift-version: [5.7.3, 5.8, 5.9] + swift-version: [5.9] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set Ubuntu Release DOT run: echo "RELEASE_DOT=$(lsb_release -sr)" >> $GITHUB_ENV - name: Set Ubuntu Release NUM @@ -24,7 +24,7 @@ jobs: run: echo "RELEASE_NAME=$(lsb_release -sc)" >> $GITHUB_ENV - name: Cache swift package modules id: cache-spm-linux - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-spm with: @@ -35,7 +35,7 @@ jobs: ${{ runner.os }}-${{ env.RELEASE_DOT }}-${{ env.cache-name }}- - name: Cache swift id: cache-swift-linux - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-swift with: @@ -51,35 +51,50 @@ jobs: run: tar xzf swift-${SWIFT_VER}-RELEASE-ubuntu${RELEASE_DOT}.tar.gz - name: Add Path run: echo "$GITHUB_WORKSPACE/swift-${SWIFT_VER}-RELEASE-ubuntu${RELEASE_DOT}/usr/bin" >> $GITHUB_PATH - - name: Build - run: swift build + - name: Test + run: swift test --enable-code-coverage + - uses: sersoft-gmbh/swift-coverage-action@v4 + id: coverage-files + with: + fail-on-empty-output: true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + flags: swift-${{ matrix.swift-version }},ubuntu-${{ matrix.RELEASE_DOT }} + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }} build-macos: name: Build on macOS - runs-on: macos-13 + runs-on: ${{ matrix.os }} if: "!contains(github.event.head_commit.message, 'ci skip')" strategy: matrix: include: - - xcode: "/Applications/Xcode_14.2.app" - iOSVersion: "16.2" - watchOSVersion: "9.1" - watchName: "Apple Watch Ultra (49mm)" - iPhoneName: "iPhone 14" - - xcode: "/Applications/Xcode_14.3.1.app" - iOSVersion: "16.4" - watchOSVersion: "9.4" - watchName: "Apple Watch Ultra (49mm)" - iPhoneName: "iPhone 14 Pro Max" - xcode: "/Applications/Xcode_15.0.1.app" - iOSVersion: "17.0" + os: macos-13 + iOSVersion: "17.0.1" watchOSVersion: "10.0" - watchName: "Apple Watch Ultra 2 (49mm)" - iPhoneName: "iPhone 14 Pro Max" + watchName: "Apple Watch Series 9 (41mm)" + iPhoneName: "iPhone 15 Pro" + - xcode: "/Applications/Xcode_15.1.app" + os: macos-13 + iOSVersion: "17.2" + watchOSVersion: "10.2" + watchName: "Apple Watch Series 9 (45mm)" + iPhoneName: "iPhone 15 Pro" + - xcode: "/Applications/Xcode_15.2.app" + os: macos-14 + iOSVersion: "17.2" + watchOSVersion: "10.2" + watchName: "Apple Watch Ultra (49mm)" + iPhoneName: "iPhone 15 Pro Max" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Cache swift package modules id: cache-spm-macos - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-spm with: @@ -88,9 +103,9 @@ jobs: restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.xcode }}- - name: Cache mint - if: startsWith(matrix.xcode,'/Applications/Xcode_15.0.1') + if: startsWith(matrix.xcode,'/Applications/Xcode_15.2') id: cache-mint - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-mint with: @@ -105,15 +120,29 @@ jobs: - name: Setup Xcode run: sudo xcode-select -s ${{ matrix.xcode }}/Contents/Developer - name: Install mint - if: startsWith(matrix.xcode,'/Applications/Xcode_15.0.1') + if: startsWith(matrix.xcode,'/Applications/Xcode_15.2') run: | brew update brew install mint - name: Build run: swift build + # - name: Run Swift Package tests + # run: swift test -v --enable-code-coverage + # - uses: sersoft-gmbh/swift-coverage-action@v4 + # id: coverage-files + # with: + # fail-on-empty-output: true + # - name: Upload SPM coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # fail_ci_if_error: true + # flags: SPM + # verbose: true + # token: ${{ secrets.CODECOV_TOKEN }} + # files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }} - name: Lint run: ./scripts/lint.sh - if: startsWith(matrix.xcode,'/Applications/Xcode_15.0.1') + if: startsWith(matrix.xcode,'/Applications/Xcode_15.2') # - name: Dump PIF # if: startsWith(matrix.xcode,'/Applications/Xcode_14') # run: | @@ -125,22 +154,22 @@ jobs: # ATTEMPT=$(($ATTEMPT+1)) # done - name: Run iOS target tests - run: xcodebuild build -scheme Sublimation -sdk iphonesimulator -destination 'platform=iOS Simulator,name=${{ matrix.iPhoneName}},OS=${{ matrix.iOSVersion }}' - # - uses: sersoft-gmbh/swift-coverage-action@v2 - # - name: Upload iOS coverage to Codecov - # uses: codecov/codecov-action@v2 - # with: - # fail_ci_if_error: true - # flags: iOS,iOS-${{ matrix.iOSVersion }} - # verbose: true - # token: ${{ secrets.CODECOV_TOKEN }} + run: xcodebuild build test -scheme Sublimation-Package -sdk iphonesimulator -destination 'platform=iOS Simulator,name=${{ matrix.iPhoneName}},OS=${{ matrix.iOSVersion }}' + - uses: sersoft-gmbh/swift-coverage-action@v4 + - name: Upload iOS coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + flags: iOS,iOS-${{ matrix.iOSVersion }} + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} - name: Run watchOS target tests - run: xcodebuild build -scheme Sublimation -sdk watchsimulator -destination 'platform=watchOS Simulator,name=${{ matrix.watchName }},OS=${{ matrix.watchOSVersion }}' - # - uses: sersoft-gmbh/swift-coverage-action@v2 - # - name: Upload watchOS coverage to Codecov - # uses: codecov/codecov-action@v2 - # with: - # fail_ci_if_error: true - # flags: watchOS,watchOS${{ matrix.watchOSVersion }} - # verbose: true - # token: ${{ secrets.CODECOV_TOKEN }} + run: xcodebuild build test -scheme Sublimation-Package -sdk watchsimulator -destination 'platform=watchOS Simulator,name=${{ matrix.watchName }},OS=${{ matrix.watchOSVersion }}' + - uses: sersoft-gmbh/swift-coverage-action@v4 + - name: Upload watchOS coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + flags: watchOS,watchOS${{ matrix.watchOSVersion }} + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8b64b13..d361ce5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,7 +29,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners # Consider using larger runners for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + runs-on: ${{ (matrix.language == 'swift' && 'macos-13') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: actions: read @@ -47,11 +47,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Setup Xcode + run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -64,20 +67,11 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - - name: Build - run: swift build - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + - run: | + echo "Run, Build Application using script" + swift build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.periphery.yml b/.periphery.yml deleted file mode 100644 index 9e26dfe..0000000 --- a/.periphery.yml +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/.strict.stringslint.yml b/.strict.stringslint.yml deleted file mode 100644 index 3431991..0000000 --- a/.strict.stringslint.yml +++ /dev/null @@ -1,17 +0,0 @@ -included: # paths to include during linting. `--path` is ignored if present. -- Sources -# Customize specific rules -missing: - severity: error - -partial: - severity: error - -unused: - severity: none - -missing_comment: - severity: error - -json_comment_rule: - severity: none \ No newline at end of file diff --git a/.stringslint.yml b/.stringslint.yml deleted file mode 100644 index 3ba25ff..0000000 --- a/.stringslint.yml +++ /dev/null @@ -1,17 +0,0 @@ -included: # paths to include during linting. `--path` is ignored if present. -- Sources -# Customize specific rules -missing: - severity: error - -partial: - severity: warning - -unused: - severity: none - -missing_comment: - severity: warning - -json_comment_rule: - severity: none \ No newline at end of file diff --git a/.swiftformat b/.swiftformat index 1279e98..e233251 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,7 +1,7 @@ --indent 2 ---header strip +--header "\n .*?\.swift\n Sublimation\n\n Created by Leo Dion.\n Copyright © {year} BrightDigit.\n\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the “Software”), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n \n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n" --commas inline ---disable wrapMultilineStatementBraces +--disable wrapMultilineStatementBraces, redundantInternal --extensionacl on-declarations --decimalgrouping 3,4 ---exclude .build, DerivedData, .swiftpm +--exclude .build, DerivedData, .swiftpm, Sources/NgrokOpenAPIClient, Demo diff --git a/.swiftlint.yml b/.swiftlint.yml index bcdf6d4..301f075 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,5 +1,4 @@ opt_in_rules: - - anyobject_protocol - array_init - attributes - closure_body_length @@ -22,14 +21,13 @@ opt_in_rules: - expiring_todo - explicit_acl - explicit_init - - explicit_self - explicit_top_level_acl - fallthrough - fatal_error_message - - file_header - file_name - file_name_no_space - file_types_order + - first_where - flatmap_over_map_reduce - force_unwrapping - function_default_parameter_at_end @@ -44,9 +42,9 @@ opt_in_rules: - legacy_random - literal_expression_end_indentation - lower_acl_than_parent - #- missing_docs + - missing_docs - modifier_order - #- multiline_arguments + - multiline_arguments - multiline_arguments_brackets - multiline_function_chains - multiline_literal_brackets @@ -82,19 +80,22 @@ opt_in_rules: - strong_iboutlet - switch_case_on_newline - toggle_bool - #- trailing_closure + - trailing_closure - type_contents_order - unavailable_function - unneeded_parentheses_in_closure_argument - unowned_variable_capture - - unused_declaration - - unused_import + - untyped_error_in_catch - vertical_parameter_alignment_on_call - vertical_whitespace_between_cases - vertical_whitespace_closing_braces - vertical_whitespace_opening_braces - xct_specific_matcher - yoda_condition +analyzer_rules: + - explicit_self + - unused_declaration + - unused_import cyclomatic_complexity: - 6 - 9 @@ -105,21 +106,22 @@ file_length: - 400 - 800 function_body_length: - - 25 + - 26 - 40 function_parameter_count: 8 line_length: - 90 - 90 +generic_type_name: + max_length: 40 identifier_name: excluded: - id excluded: - - Tests - DerivedData - .build - .swiftpm - - Demo/SublimationDemoServer/.build - - Demo/SublimationDemoServer/.swiftpm + - Demo + - Sources/NgrokOpenAPIClient indentation_width: indentation_width: 2 diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Demo/SublimationDemoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/SublimationDemoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5a93aae..52eaef5 100644 --- a/Demo/SublimationDemoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Demo/SublimationDemoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "333e60cc90f52973f7ee29cd8e3a7f6adfe79f4e", - "version" : "1.17.0" + "revision" : "291438696abdd48d2a83b52465c176efbd94512b", + "version" : "1.20.1" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/console-kit.git", "state" : { - "revision" : "a7e67a1719933318b5ab7eaaed355cde020465b1", - "version" : "4.5.0" + "revision" : "a31f44ebfbd15a2cc0fda705279676773ac16355", + "version" : "4.14.1" } }, { @@ -37,30 +37,12 @@ } }, { - "identity" : "prch", - "kind" : "remoteSourceControl", - "location" : "https://github.com/brightdigit/Prch.git", - "state" : { - "revision" : "b9329bab762886dbc478c9f6850a63bfde7fde93", - "version" : "1.0.0-alpha.2" - } - }, - { - "identity" : "prchnio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/brightdigit/PrchNIO.git", - "state" : { - "revision" : "939b21a3e8b34d6dd0746d429ef97254f3a2445a", - "version" : "1.0.0-alpha.1" - } - }, - { - "identity" : "prchvapor", + "identity" : "openapikit", "kind" : "remoteSourceControl", - "location" : "https://github.com/brightdigit/PrchVapor.git", + "location" : "https://github.com/mattpolzin/OpenAPIKit", "state" : { - "revision" : "8f784d12ab5197f04977b318498e8cfd04b7edc9", - "version" : "1.0.0-alpha.1" + "revision" : "283454875cc6e5b2801d184d65835b92252d1784", + "version" : "3.1.2" } }, { @@ -68,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/routing-kit.git", "state" : { - "revision" : "ffac7b3a127ce1e85fb232f1a6271164628809ad", - "version" : "4.6.0" + "revision" : "2a92a7eac411a82fb3a03731be5e76773ebe1b3e", + "version" : "4.9.0" } }, { @@ -77,26 +59,26 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-algorithms.git", "state" : { - "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", - "version" : "1.0.0" + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" } }, { - "identity" : "swift-atomics", + "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { - "revision" : "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", - "version" : "1.0.2" + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" } }, { - "identity" : "swift-backtrace", + "identity" : "swift-atomics", "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-server/swift-backtrace.git", + "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "f25620d5d05e2f1ba27154b40cafea2b67566956", - "version" : "1.3.3" + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" } }, { @@ -117,13 +99,22 @@ "version" : "2.2.0" } }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "12358d55a3824bd5fed310b999ea8cf83a9a1a65", + "version" : "1.0.3" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", - "version" : "1.4.4" + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" } }, { @@ -140,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "e0cc6dd6ffa8e6a6f565938acd858b24e47902d0", - "version" : "2.50.0" + "revision" : "635b2589494c97e48c62514bc8b37ced762e0a62", + "version" : "2.63.0" } }, { @@ -149,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "91dd2d61fb772e1311bb5f13b59266b579d77e42", - "version" : "1.15.0" + "revision" : "363da63c1966405764f380c627409b2f9d9e710b", + "version" : "1.21.0" } }, { @@ -158,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "d6656967f33ed8b368b38e4b198631fc7c484a40", - "version" : "1.23.1" + "revision" : "0904bf0feb5122b7e5c3f15db7df0eabe623dd87", + "version" : "1.30.0" } }, { @@ -167,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "4fb7ead803e38949eb1d6fabb849206a72c580f3", - "version" : "2.23.0" + "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", + "version" : "2.26.0" } }, { @@ -176,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "c0d9a144cfaec8d3d596aadde3039286a266c15c", - "version" : "1.15.0" + "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce", + "version" : "1.20.1" } }, { @@ -189,13 +180,58 @@ "version" : "1.0.2" } }, + { + "identity" : "swift-openapi-async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-openapi-async-http-client", + "state" : { + "revision" : "abfe558a66992ef1e896a577010f957915f30591", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-openapi-generator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-openapi-generator", + "state" : { + "revision" : "b18becec9d33b8a23d6252b47bb6bf1d45304244", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-openapi-runtime", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-openapi-runtime", + "state" : { + "revision" : "76951d77a0609599d2dc233e7e40808a74767c6a", + "version" : "1.3.2" + } + }, + { + "identity" : "swift-openapi-urlsession", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-openapi-urlsession", + "state" : { + "revision" : "6efbfda5276bbbc8b4fec5d744f0ecd8c784eb47", + "version" : "1.0.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", + "version" : "1.2.1" + } + }, { "identity" : "vapor", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "f4b00a5350238fe896d865d96d64f12fcbbeda95", - "version" : "4.76.0" + "revision" : "4942d74e8493fc918ed6144c835c8a0e6affd4f4", + "version" : "4.92.1" } }, { @@ -203,8 +239,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/websocket-kit.git", "state" : { - "revision" : "2d9d2188a08eef4a869d368daab21b3c08510991", - "version" : "2.6.1" + "revision" : "53fe0639a98903858d0196b699720decb42aee7b", + "version" : "2.14.0" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", + "version" : "5.0.6" } } ], diff --git a/Demo/SublimationDemoApp/ContentView.swift b/Demo/SublimationDemoApp/ContentView.swift index e0806f2..38b05b1 100644 --- a/Demo/SublimationDemoApp/ContentView.swift +++ b/Demo/SublimationDemoApp/ContentView.swift @@ -3,7 +3,7 @@ import SublimationDemoConfiguration import SwiftUI extension View { - func taskPolyfill(_ action: @escaping @Sendable() async -> Void) -> some View { + func taskPolyfill(_ action: @escaping @Sendable () async -> Void) -> some View { if #available(iOS 15.0, *) { return self.task(action) } else { @@ -72,17 +72,17 @@ struct ContentView: View { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) - Text(self.serverResponse) + Text(serverResponse) } .padding() .taskPolyfill { let serverResponse: String do { - let url = try await self.getBaseURL( + let url = try await getBaseURL( fromBucket: Configuration.bucketName, withKey: Configuration.key ) - serverResponse = try await self.getServerResponse(from: url) + serverResponse = try await getServerResponse(from: url) } catch { serverResponse = error.localizedDescription } diff --git a/Demo/SublimationDemoServer/Package.swift b/Demo/SublimationDemoServer/Package.swift index 9448057..d3a8e78 100644 --- a/Demo/SublimationDemoServer/Package.swift +++ b/Demo/SublimationDemoServer/Package.swift @@ -1,10 +1,10 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "SublimationDemoServer", - platforms: [.macOS(.v12), .iOS(.v14), .watchOS(.v7)], + platforms: [.macOS(.v14), .iOS(.v17), .watchOS(.v10)], products: [ .executable( name: "SublimationDemoServer", diff --git a/Mintfile b/Mintfile index e9227c0..d27e89c 100644 --- a/Mintfile +++ b/Mintfile @@ -1,4 +1,2 @@ -Faire/StringsLint@1.2.0 -nicklockwood/SwiftFormat@0.47.0 -realm/SwiftLint@0.41.0 -peripheryapp/periphery@2.10.0 \ No newline at end of file +nicklockwood/SwiftFormat@0.53.1 +realm/SwiftLint@0.54.0 \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index f8502c2..43777bb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "16f7e62c08c6969899ce6cc277041e868364e5cf", - "version" : "1.19.0" + "revision" : "291438696abdd48d2a83b52465c176efbd94512b", + "version" : "1.20.1" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/console-kit.git", "state" : { - "revision" : "2e3e205e8d7563d5c1a6f1c8992616d337f632e6", - "version" : "4.11.0" + "revision" : "a31f44ebfbd15a2cc0fda705279676773ac16355", + "version" : "4.14.1" } }, { @@ -32,35 +32,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/multipart-kit.git", "state" : { - "revision" : "1adfd69df2da08f7931d4281b257475e32c96734", - "version" : "4.5.4" - } - }, - { - "identity" : "prch", - "kind" : "remoteSourceControl", - "location" : "https://github.com/brightdigit/Prch.git", - "state" : { - "revision" : "b9329bab762886dbc478c9f6850a63bfde7fde93", - "version" : "1.0.0-alpha.2" - } - }, - { - "identity" : "prchnio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/brightdigit/PrchNIO.git", - "state" : { - "revision" : "939b21a3e8b34d6dd0746d429ef97254f3a2445a", - "version" : "1.0.0-alpha.1" + "revision" : "12ee56f25bd3fc4c2d09c2aa16e69de61dc786e8", + "version" : "4.6.0" } }, { - "identity" : "prchvapor", + "identity" : "openapikit", "kind" : "remoteSourceControl", - "location" : "https://github.com/brightdigit/PrchVapor.git", + "location" : "https://github.com/mattpolzin/OpenAPIKit", "state" : { - "revision" : "8f784d12ab5197f04977b318498e8cfd04b7edc9", - "version" : "1.0.0-alpha.1" + "revision" : "283454875cc6e5b2801d184d65835b92252d1784", + "version" : "3.1.2" } }, { @@ -68,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/routing-kit.git", "state" : { - "revision" : "17a7a3facce8285fd257aa7c72d5e480351e7698", - "version" : "4.8.2" + "revision" : "2a92a7eac411a82fb3a03731be5e76773ebe1b3e", + "version" : "4.9.0" } }, { @@ -81,6 +63,15 @@ "version" : "1.2.0" } }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -93,10 +84,10 @@ { "identity" : "swift-collections", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", + "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", - "version" : "1.0.5" + "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", + "version" : "1.0.6" } }, { @@ -104,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "b51f1d6845b353a2121de1c6a670738ec33561a6", - "version" : "3.1.0" + "revision" : "cc76b894169a3c86b71bac10c78a4db6beb7a9ad", + "version" : "3.2.0" } }, { @@ -113,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types", "state" : { - "revision" : "99d066e29effa8845e4761dd3f2f831edfdf8925", - "version" : "1.0.0" + "revision" : "12358d55a3824bd5fed310b999ea8cf83a9a1a65", + "version" : "1.0.3" } }, { @@ -122,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" } }, { @@ -138,10 +129,10 @@ { "identity" : "swift-nio", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", + "location" : "https://github.com/apple/swift-nio", "state" : { - "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", - "version" : "2.62.0" + "revision" : "635b2589494c97e48c62514bc8b37ced762e0a62", + "version" : "2.63.0" } }, { @@ -149,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", - "version" : "1.20.0" + "revision" : "363da63c1966405764f380c627409b2f9d9e710b", + "version" : "1.21.0" } }, { @@ -158,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806", - "version" : "1.29.0" + "revision" : "0904bf0feb5122b7e5c3f15db7df0eabe623dd87", + "version" : "1.30.0" } }, { @@ -167,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", - "version" : "2.25.0" + "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", + "version" : "2.26.0" } }, { @@ -176,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", - "version" : "1.20.0" + "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce", + "version" : "1.20.1" } }, { @@ -189,13 +180,49 @@ "version" : "1.0.2" } }, + { + "identity" : "swift-openapi-async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-openapi-async-http-client", + "state" : { + "revision" : "abfe558a66992ef1e896a577010f957915f30591", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-openapi-generator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-openapi-generator", + "state" : { + "revision" : "b18becec9d33b8a23d6252b47bb6bf1d45304244", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-openapi-runtime", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-openapi-runtime", + "state" : { + "revision" : "76951d77a0609599d2dc233e7e40808a74767c6a", + "version" : "1.3.2" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", + "version" : "1.2.1" + } + }, { "identity" : "vapor", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "da9c2805b1b93751b1922bbbf343aa120e9942c8", - "version" : "4.87.1" + "revision" : "4942d74e8493fc918ed6144c835c8a0e6affd4f4", + "version" : "4.92.1" } }, { @@ -206,6 +233,15 @@ "revision" : "53fe0639a98903858d0196b699720decb42aee7b", "version" : "2.14.0" } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", + "version" : "5.0.6" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 17ed175..cd558fa 100644 --- a/Package.swift +++ b/Package.swift @@ -1,29 +1,111 @@ -// swift-tools-version: 5.7 - +// swift-tools-version: 5.9 +// swiftlint:disable explicit_acl explicit_top_level_acl import PackageDescription +let swiftSettings: [SwiftSetting] = [ +// .enableUpcomingFeature("BareSlashRegexLiterals"), +// .enableUpcomingFeature("ConciseMagicFile"), +// .enableUpcomingFeature("ExistentialAny"), +// .enableUpcomingFeature("ForwardTrailingClosures"), +// .enableUpcomingFeature("ImplicitOpenExistentials"), +// .enableUpcomingFeature("StrictConcurrency") +// .unsafeFlags(["-warn-concurrency", "-enable-actor-data-race-checks"]) +] + let package = Package( name: "Sublimation", - platforms: [.macOS(.v12), .iOS(.v14), .watchOS(.v7)], + platforms: [ + .macOS(.v14), + .iOS(.v17), + .watchOS(.v10), + .tvOS(.v17), + .visionOS(.v1), + .macCatalyst(.v17) + ], products: [ .library(name: "Sublimation", targets: ["Sublimation"]), .library(name: "SublimationVapor", targets: ["SublimationVapor"]), .library(name: "Ngrokit", targets: ["Ngrokit"]) ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), - .package(url: "https://github.com/vapor/vapor.git", from: "4.66.0"), - .package(url: "https://github.com/brightdigit/Prch.git", from: "1.0.0-alpha.1"), - .package(url: "https://github.com/brightdigit/PrchVapor.git", from: "1.0.0-alpha.1") + .package( + url: "https://github.com/vapor/vapor.git", + from: "4.92.0" + ), + .package( + url: "https://github.com/apple/swift-openapi-generator", + from: "1.0.0" + ), + .package( + url: "https://github.com/apple/swift-openapi-runtime", + from: "1.0.0" + ), + .package( + url: "https://github.com/swift-server/swift-openapi-async-http-client", + from: "1.0.0" + ) ], targets: [ - .target(name: "Ngrokit", dependencies: ["Prch"]), - .target(name: "Sublimation"), - .target(name: "SublimationVapor", - dependencies: [ - "Ngrokit", "PrchVapor", "Sublimation", - .product(name: "Vapor", package: "vapor") - ]) + .target( + name: "NgrokOpenAPIClient", + dependencies: [ + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime") + ] + ), + .target( + name: "Ngrokit", + dependencies: [ + "NgrokOpenAPIClient", + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime") + ], + swiftSettings: swiftSettings + ), + .target( + name: "Sublimation", + swiftSettings: swiftSettings + ), + .target( + name: "SublimationVapor", + dependencies: [ + .product( + name: "OpenAPIAsyncHTTPClient", + package: "swift-openapi-async-http-client" + ), + "Ngrokit", + "Sublimation", + .product( + name: "Vapor", + package: "vapor" + ) + ], + swiftSettings: swiftSettings + ), + .target( + name: "NgrokitMocks", + dependencies: ["Ngrokit"] + ), + .testTarget( + name: "NgrokitTests", + dependencies: ["Ngrokit", "NgrokitMocks"], + swiftSettings: swiftSettings + ), + .target( + name: "SublimationMocks", + dependencies: ["Sublimation"] + ), + .testTarget( + name: "SublimationTests", + dependencies: ["Sublimation", "SublimationMocks"], + swiftSettings: swiftSettings + ), + .testTarget( + name: "SublimationVaporTests", + dependencies: [ + "SublimationVapor", + "NgrokitMocks" + ], + swiftSettings: swiftSettings + ) ] ) +// swiftlint:enable explicit_acl explicit_top_level_acl diff --git a/Sources/NgrokOpenAPIClient/Client.swift b/Sources/NgrokOpenAPIClient/Client.swift new file mode 100644 index 0000000..103e3f2 --- /dev/null +++ b/Sources/NgrokOpenAPIClient/Client.swift @@ -0,0 +1,305 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import struct Foundation.Date +#else +import struct Foundation.URL +import struct Foundation.Data +import struct Foundation.Date +#endif +import HTTPTypes +package struct Client: APIProtocol { + /// The underlying HTTP client. + private let client: UniversalClient + /// Creates a new client. + /// - Parameters: + /// - serverURL: The server URL that the client connects to. Any server + /// URLs defined in the OpenAPI document are available as static methods + /// on the ``Servers`` type. + /// - configuration: A set of configuration values for the client. + /// - transport: A transport that performs HTTP operations. + /// - middlewares: A list of middlewares to call before the transport. + package init( + serverURL: Foundation.URL, + configuration: Configuration = .init(), + transport: any ClientTransport, + middlewares: [any ClientMiddleware] = [] + ) { + self.client = .init( + serverURL: serverURL, + configuration: configuration, + transport: transport, + middlewares: middlewares + ) + } + private var converter: Converter { + client.converter + } + /// Access the root API resource of a running ngrok agent + /// + /// - Remark: HTTP `GET /api`. + /// - Remark: Generated from `#/paths//api/get`. + package func get_sol_api(_ input: Operations.get_sol_api.Input) async throws -> Operations.get_sol_api.Output { + try await client.send( + input: input, + forOperation: Operations.get_sol_api.id, + serializer: { input in + let path = try converter.renderedPath( + template: "/api", + parameters: [] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .get + ) + suppressMutabilityWarning(&request) + return (request, nil) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 200: + return .ok(.init()) + default: + return .undocumented( + statusCode: response.status.code, + .init( + headerFields: response.headerFields, + body: responseBody + ) + ) + } + } + ) + } + /// List Tunnels + /// + /// - Remark: HTTP `GET /api/tunnels`. + /// - Remark: Generated from `#/paths//api/tunnels/get(listTunnels)`. + package func listTunnels(_ input: Operations.listTunnels.Input) async throws -> Operations.listTunnels.Output { + try await client.send( + input: input, + forOperation: Operations.listTunnels.id, + serializer: { input in + let path = try converter.renderedPath( + template: "/api/tunnels", + parameters: [] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .get + ) + suppressMutabilityWarning(&request) + converter.setAcceptHeader( + in: &request.headerFields, + contentTypes: input.headers.accept + ) + return (request, nil) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 200: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.listTunnels.Output.Ok.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.TunnelList.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .ok(.init(body: body)) + default: + return .undocumented( + statusCode: response.status.code, + .init( + headerFields: response.headerFields, + body: responseBody + ) + ) + } + } + ) + } + /// Start tunnel + /// + /// - Remark: HTTP `POST /api/tunnels`. + /// - Remark: Generated from `#/paths//api/tunnels/post(startTunnel)`. + package func startTunnel(_ input: Operations.startTunnel.Input) async throws -> Operations.startTunnel.Output { + try await client.send( + input: input, + forOperation: Operations.startTunnel.id, + serializer: { input in + let path = try converter.renderedPath( + template: "/api/tunnels", + parameters: [] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .post + ) + suppressMutabilityWarning(&request) + converter.setAcceptHeader( + in: &request.headerFields, + contentTypes: input.headers.accept + ) + let body: OpenAPIRuntime.HTTPBody? + switch input.body { + case let .json(value): + body = try converter.setRequiredRequestBodyAsJSON( + value, + headerFields: &request.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return (request, body) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 201: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.startTunnel.Output.Created.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.TunnelResponse.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .created(.init(body: body)) + default: + return .undocumented( + statusCode: response.status.code, + .init( + headerFields: response.headerFields, + body: responseBody + ) + ) + } + } + ) + } + /// Tunnel detail + /// + /// - Remark: HTTP `GET /api/tunnels/{name}`. + /// - Remark: Generated from `#/paths//api/tunnels/{name}/get(getTunnel)`. + package func getTunnel(_ input: Operations.getTunnel.Input) async throws -> Operations.getTunnel.Output { + try await client.send( + input: input, + forOperation: Operations.getTunnel.id, + serializer: { input in + let path = try converter.renderedPath( + template: "/api/tunnels/{}", + parameters: [ + input.path.name + ] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .get + ) + suppressMutabilityWarning(&request) + converter.setAcceptHeader( + in: &request.headerFields, + contentTypes: input.headers.accept + ) + return (request, nil) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 200: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.getTunnel.Output.Ok.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + OpenAPIRuntime.OpenAPIValueContainer.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .ok(.init(body: body)) + default: + return .undocumented( + statusCode: response.status.code, + .init( + headerFields: response.headerFields, + body: responseBody + ) + ) + } + } + ) + } + /// Stop tunnel + /// + /// - Remark: HTTP `DELETE /api/tunnels/{name}`. + /// - Remark: Generated from `#/paths//api/tunnels/{name}/delete(stopTunnel)`. + package func stopTunnel(_ input: Operations.stopTunnel.Input) async throws -> Operations.stopTunnel.Output { + try await client.send( + input: input, + forOperation: Operations.stopTunnel.id, + serializer: { input in + let path = try converter.renderedPath( + template: "/api/tunnels/{}", + parameters: [ + input.path.name + ] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .delete + ) + suppressMutabilityWarning(&request) + return (request, nil) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 204: + return .noContent(.init()) + default: + return .undocumented( + statusCode: response.status.code, + .init( + headerFields: response.headerFields, + body: responseBody + ) + ) + } + } + ) + } +} diff --git a/Sources/NgrokOpenAPIClient/Types.swift b/Sources/NgrokOpenAPIClient/Types.swift new file mode 100644 index 0000000..2984cc7 --- /dev/null +++ b/Sources/NgrokOpenAPIClient/Types.swift @@ -0,0 +1,846 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import struct Foundation.Date +#else +import struct Foundation.URL +import struct Foundation.Data +import struct Foundation.Date +#endif +/// A type that performs HTTP operations defined by the OpenAPI document. +package protocol APIProtocol: Sendable { + /// Access the root API resource of a running ngrok agent + /// + /// - Remark: HTTP `GET /api`. + /// - Remark: Generated from `#/paths//api/get`. + func get_sol_api(_ input: Operations.get_sol_api.Input) async throws -> Operations.get_sol_api.Output + /// List Tunnels + /// + /// - Remark: HTTP `GET /api/tunnels`. + /// - Remark: Generated from `#/paths//api/tunnels/get(listTunnels)`. + func listTunnels(_ input: Operations.listTunnels.Input) async throws -> Operations.listTunnels.Output + /// Start tunnel + /// + /// - Remark: HTTP `POST /api/tunnels`. + /// - Remark: Generated from `#/paths//api/tunnels/post(startTunnel)`. + func startTunnel(_ input: Operations.startTunnel.Input) async throws -> Operations.startTunnel.Output + /// Tunnel detail + /// + /// - Remark: HTTP `GET /api/tunnels/{name}`. + /// - Remark: Generated from `#/paths//api/tunnels/{name}/get(getTunnel)`. + func getTunnel(_ input: Operations.getTunnel.Input) async throws -> Operations.getTunnel.Output + /// Stop tunnel + /// + /// - Remark: HTTP `DELETE /api/tunnels/{name}`. + /// - Remark: Generated from `#/paths//api/tunnels/{name}/delete(stopTunnel)`. + func stopTunnel(_ input: Operations.stopTunnel.Input) async throws -> Operations.stopTunnel.Output +} + +/// Convenience overloads for operation inputs. +extension APIProtocol { + /// Access the root API resource of a running ngrok agent + /// + /// - Remark: HTTP `GET /api`. + /// - Remark: Generated from `#/paths//api/get`. + package func get_sol_api() async throws -> Operations.get_sol_api.Output { + try await get_sol_api(Operations.get_sol_api.Input()) + } + /// List Tunnels + /// + /// - Remark: HTTP `GET /api/tunnels`. + /// - Remark: Generated from `#/paths//api/tunnels/get(listTunnels)`. + package func listTunnels(headers: Operations.listTunnels.Input.Headers = .init()) async throws -> Operations.listTunnels.Output { + try await listTunnels(Operations.listTunnels.Input(headers: headers)) + } + /// Start tunnel + /// + /// - Remark: HTTP `POST /api/tunnels`. + /// - Remark: Generated from `#/paths//api/tunnels/post(startTunnel)`. + package func startTunnel( + headers: Operations.startTunnel.Input.Headers = .init(), + body: Operations.startTunnel.Input.Body + ) async throws -> Operations.startTunnel.Output { + try await startTunnel(Operations.startTunnel.Input( + headers: headers, + body: body + )) + } + /// Tunnel detail + /// + /// - Remark: HTTP `GET /api/tunnels/{name}`. + /// - Remark: Generated from `#/paths//api/tunnels/{name}/get(getTunnel)`. + package func getTunnel( + path: Operations.getTunnel.Input.Path, + headers: Operations.getTunnel.Input.Headers = .init() + ) async throws -> Operations.getTunnel.Output { + try await getTunnel(Operations.getTunnel.Input( + path: path, + headers: headers + )) + } + /// Stop tunnel + /// + /// - Remark: HTTP `DELETE /api/tunnels/{name}`. + /// - Remark: Generated from `#/paths//api/tunnels/{name}/delete(stopTunnel)`. + package func stopTunnel(path: Operations.stopTunnel.Input.Path) async throws -> Operations.stopTunnel.Output { + try await stopTunnel(Operations.stopTunnel.Input(path: path)) + } +} + +/// Server URLs defined in the OpenAPI document. +package enum Servers { + /// Default Local Server + package static func server1() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "http://127.0.0.1:4040", + variables: [] + ) + } +} + +/// Types generated from the components section of the OpenAPI document. +package enum Components { + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + package enum Schemas { + /// - Remark: Generated from `#/components/schemas/TunnelList`. + package struct TunnelList: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/TunnelList/tunnels`. + package var tunnels: [Components.Schemas.TunnelResponse] + /// Creates a new `TunnelList`. + /// + /// - Parameters: + /// - tunnels: + package init(tunnels: [Components.Schemas.TunnelResponse]) { + self.tunnels = tunnels + } + package enum CodingKeys: String, CodingKey { + case tunnels + } + } + /// - Remark: Generated from `#/components/schemas/TunnelRequest`. + package struct TunnelRequest: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/TunnelRequest/addr`. + package var addr: Swift.String + /// - Remark: Generated from `#/components/schemas/TunnelRequest/proto`. + package var proto: Swift.String + /// - Remark: Generated from `#/components/schemas/TunnelRequest/name`. + package var name: Swift.String + /// Creates a new `TunnelRequest`. + /// + /// - Parameters: + /// - addr: + /// - proto: + /// - name: + package init( + addr: Swift.String, + proto: Swift.String, + name: Swift.String + ) { + self.addr = addr + self.proto = proto + self.name = name + } + package enum CodingKeys: String, CodingKey { + case addr + case proto + case name + } + } + /// - Remark: Generated from `#/components/schemas/TunnelResponse`. + package struct TunnelResponse: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/TunnelResponse/name`. + package var name: Swift.String + /// - Remark: Generated from `#/components/schemas/TunnelResponse/uri`. + package var uri: Swift.String? + /// - Remark: Generated from `#/components/schemas/TunnelResponse/public_url`. + package var public_url: Swift.String + /// - Remark: Generated from `#/components/schemas/TunnelResponse/proto`. + package var proto: Swift.String? + /// - Remark: Generated from `#/components/schemas/TunnelResponse/config`. + package struct configPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/TunnelResponse/config/addr`. + package var addr: Swift.String + /// - Remark: Generated from `#/components/schemas/TunnelResponse/config/inspect`. + package var inspect: Swift.Bool + /// Creates a new `configPayload`. + /// + /// - Parameters: + /// - addr: + /// - inspect: + package init( + addr: Swift.String, + inspect: Swift.Bool + ) { + self.addr = addr + self.inspect = inspect + } + package enum CodingKeys: String, CodingKey { + case addr + case inspect + } + } + /// - Remark: Generated from `#/components/schemas/TunnelResponse/config`. + package var config: Components.Schemas.TunnelResponse.configPayload + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics`. + package struct metricsPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns`. + package struct connsPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns/count`. + package var count: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns/gauge`. + package var gauge: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns/rate1`. + package var rate1: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns/rate5`. + package var rate5: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns/rate15`. + package var rate15: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns/p50`. + package var p50: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns/p90`. + package var p90: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns/p95`. + package var p95: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns/p99`. + package var p99: Swift.Int + /// Creates a new `connsPayload`. + /// + /// - Parameters: + /// - count: + /// - gauge: + /// - rate1: + /// - rate5: + /// - rate15: + /// - p50: + /// - p90: + /// - p95: + /// - p99: + package init( + count: Swift.Int, + gauge: Swift.Int, + rate1: Swift.Int, + rate5: Swift.Int, + rate15: Swift.Int, + p50: Swift.Int, + p90: Swift.Int, + p95: Swift.Int, + p99: Swift.Int + ) { + self.count = count + self.gauge = gauge + self.rate1 = rate1 + self.rate5 = rate5 + self.rate15 = rate15 + self.p50 = p50 + self.p90 = p90 + self.p95 = p95 + self.p99 = p99 + } + package enum CodingKeys: String, CodingKey { + case count + case gauge + case rate1 + case rate5 + case rate15 + case p50 + case p90 + case p95 + case p99 + } + } + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/conns`. + package var conns: Components.Schemas.TunnelResponse.metricsPayload.connsPayload? + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http`. + package struct httpPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http/count`. + package var count: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http/rate1`. + package var rate1: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http/rate5`. + package var rate5: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http/rate15`. + package var rate15: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http/p50`. + package var p50: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http/p90`. + package var p90: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http/p95`. + package var p95: Swift.Int + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http/p99`. + package var p99: Swift.Int + /// Creates a new `httpPayload`. + /// + /// - Parameters: + /// - count: + /// - rate1: + /// - rate5: + /// - rate15: + /// - p50: + /// - p90: + /// - p95: + /// - p99: + package init( + count: Swift.Int, + rate1: Swift.Int, + rate5: Swift.Int, + rate15: Swift.Int, + p50: Swift.Int, + p90: Swift.Int, + p95: Swift.Int, + p99: Swift.Int + ) { + self.count = count + self.rate1 = rate1 + self.rate5 = rate5 + self.rate15 = rate15 + self.p50 = p50 + self.p90 = p90 + self.p95 = p95 + self.p99 = p99 + } + package enum CodingKeys: String, CodingKey { + case count + case rate1 + case rate5 + case rate15 + case p50 + case p90 + case p95 + case p99 + } + } + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics/http`. + package var http: Components.Schemas.TunnelResponse.metricsPayload.httpPayload? + /// Creates a new `metricsPayload`. + /// + /// - Parameters: + /// - conns: + /// - http: + package init( + conns: Components.Schemas.TunnelResponse.metricsPayload.connsPayload? = nil, + http: Components.Schemas.TunnelResponse.metricsPayload.httpPayload? = nil + ) { + self.conns = conns + self.http = http + } + package enum CodingKeys: String, CodingKey { + case conns + case http + } + } + /// - Remark: Generated from `#/components/schemas/TunnelResponse/metrics`. + package var metrics: Components.Schemas.TunnelResponse.metricsPayload? + /// Creates a new `TunnelResponse`. + /// + /// - Parameters: + /// - name: + /// - uri: + /// - public_url: + /// - proto: + /// - config: + /// - metrics: + package init( + name: Swift.String, + uri: Swift.String? = nil, + public_url: Swift.String, + proto: Swift.String? = nil, + config: Components.Schemas.TunnelResponse.configPayload, + metrics: Components.Schemas.TunnelResponse.metricsPayload? = nil + ) { + self.name = name + self.uri = uri + self.public_url = public_url + self.proto = proto + self.config = config + self.metrics = metrics + } + package enum CodingKeys: String, CodingKey { + case name + case uri + case public_url + case proto + case config + case metrics + } + } + } + /// Types generated from the `#/components/parameters` section of the OpenAPI document. + package enum Parameters {} + /// Types generated from the `#/components/requestBodies` section of the OpenAPI document. + package enum RequestBodies {} + /// Types generated from the `#/components/responses` section of the OpenAPI document. + package enum Responses {} + /// Types generated from the `#/components/headers` section of the OpenAPI document. + package enum Headers {} +} + +/// API operations, with input and output types, generated from `#/paths` in the OpenAPI document. +package enum Operations { + /// Access the root API resource of a running ngrok agent + /// + /// - Remark: HTTP `GET /api`. + /// - Remark: Generated from `#/paths//api/get`. + package enum get_sol_api { + package static let id: Swift.String = "get/api" + package struct Input: Sendable, Hashable { + /// Creates a new `Input`. + package init() {} + } + @frozen package enum Output: Sendable, Hashable { + package struct Ok: Sendable, Hashable { + /// Creates a new `Ok`. + package init() {} + } + /// Successful response + /// + /// - Remark: Generated from `#/paths//api/get/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.get_sol_api.Output.Ok) + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + package var ok: Operations.get_sol_api.Output.Ok { + get throws { + switch self { + case let .ok(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "ok", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// List Tunnels + /// + /// - Remark: HTTP `GET /api/tunnels`. + /// - Remark: Generated from `#/paths//api/tunnels/get(listTunnels)`. + package enum listTunnels { + package static let id: Swift.String = "listTunnels" + package struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/GET/header`. + package struct Headers: Sendable, Hashable { + package var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + package init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) { + self.accept = accept + } + } + package var headers: Operations.listTunnels.Input.Headers + /// Creates a new `Input`. + /// + /// - Parameters: + /// - headers: + package init(headers: Operations.listTunnels.Input.Headers = .init()) { + self.headers = headers + } + } + @frozen package enum Output: Sendable, Hashable { + package struct Ok: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/GET/responses/200/content`. + @frozen package enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/GET/responses/200/content/application\/json`. + case json(Components.Schemas.TunnelList) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + package var json: Components.Schemas.TunnelList { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + package var body: Operations.listTunnels.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - body: Received HTTP response body + package init(body: Operations.listTunnels.Output.Ok.Body) { + self.body = body + } + } + /// Successful response + /// + /// - Remark: Generated from `#/paths//api/tunnels/get(listTunnels)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.listTunnels.Output.Ok) + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + package var ok: Operations.listTunnels.Output.Ok { + get throws { + switch self { + case let .ok(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "ok", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + @frozen package enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + package init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + package var rawValue: Swift.String { + switch self { + case let .other(string): + return string + case .json: + return "application/json" + } + } + package static var allCases: [Self] { + [ + .json + ] + } + } + } + /// Start tunnel + /// + /// - Remark: HTTP `POST /api/tunnels`. + /// - Remark: Generated from `#/paths//api/tunnels/post(startTunnel)`. + package enum startTunnel { + package static let id: Swift.String = "startTunnel" + package struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/POST/header`. + package struct Headers: Sendable, Hashable { + package var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + package init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) { + self.accept = accept + } + } + package var headers: Operations.startTunnel.Input.Headers + /// - Remark: Generated from `#/paths/api/tunnels/POST/requestBody`. + @frozen package enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/POST/requestBody/content/application\/json`. + case json(Components.Schemas.TunnelRequest) + } + package var body: Operations.startTunnel.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - headers: + /// - body: + package init( + headers: Operations.startTunnel.Input.Headers = .init(), + body: Operations.startTunnel.Input.Body + ) { + self.headers = headers + self.body = body + } + } + @frozen package enum Output: Sendable, Hashable { + package struct Created: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/POST/responses/201/content`. + @frozen package enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/POST/responses/201/content/application\/json`. + case json(Components.Schemas.TunnelResponse) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + package var json: Components.Schemas.TunnelResponse { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + package var body: Operations.startTunnel.Output.Created.Body + /// Creates a new `Created`. + /// + /// - Parameters: + /// - body: Received HTTP response body + package init(body: Operations.startTunnel.Output.Created.Body) { + self.body = body + } + } + /// Tunnel started successfully + /// + /// - Remark: Generated from `#/paths//api/tunnels/post(startTunnel)/responses/201`. + /// + /// HTTP response code: `201 created`. + case created(Operations.startTunnel.Output.Created) + /// The associated value of the enum case if `self` is `.created`. + /// + /// - Throws: An error if `self` is not `.created`. + /// - SeeAlso: `.created`. + package var created: Operations.startTunnel.Output.Created { + get throws { + switch self { + case let .created(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "created", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + @frozen package enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + package init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + package var rawValue: Swift.String { + switch self { + case let .other(string): + return string + case .json: + return "application/json" + } + } + package static var allCases: [Self] { + [ + .json + ] + } + } + } + /// Tunnel detail + /// + /// - Remark: HTTP `GET /api/tunnels/{name}`. + /// - Remark: Generated from `#/paths//api/tunnels/{name}/get(getTunnel)`. + package enum getTunnel { + package static let id: Swift.String = "getTunnel" + package struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/{name}/GET/path`. + package struct Path: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/{name}/GET/path/name`. + package var name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - name: + package init(name: Swift.String) { + self.name = name + } + } + package var path: Operations.getTunnel.Input.Path + /// - Remark: Generated from `#/paths/api/tunnels/{name}/GET/header`. + package struct Headers: Sendable, Hashable { + package var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + package init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) { + self.accept = accept + } + } + package var headers: Operations.getTunnel.Input.Headers + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - headers: + package init( + path: Operations.getTunnel.Input.Path, + headers: Operations.getTunnel.Input.Headers = .init() + ) { + self.path = path + self.headers = headers + } + } + @frozen package enum Output: Sendable, Hashable { + package struct Ok: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/{name}/GET/responses/200/content`. + @frozen package enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/{name}/GET/responses/200/content/application\/json`. + case json(OpenAPIRuntime.OpenAPIValueContainer) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + package var json: OpenAPIRuntime.OpenAPIValueContainer { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + package var body: Operations.getTunnel.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - body: Received HTTP response body + package init(body: Operations.getTunnel.Output.Ok.Body) { + self.body = body + } + } + /// Successful response + /// + /// - Remark: Generated from `#/paths//api/tunnels/{name}/get(getTunnel)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.getTunnel.Output.Ok) + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + package var ok: Operations.getTunnel.Output.Ok { + get throws { + switch self { + case let .ok(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "ok", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + @frozen package enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + package init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + package var rawValue: Swift.String { + switch self { + case let .other(string): + return string + case .json: + return "application/json" + } + } + package static var allCases: [Self] { + [ + .json + ] + } + } + } + /// Stop tunnel + /// + /// - Remark: HTTP `DELETE /api/tunnels/{name}`. + /// - Remark: Generated from `#/paths//api/tunnels/{name}/delete(stopTunnel)`. + package enum stopTunnel { + package static let id: Swift.String = "stopTunnel" + package struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/{name}/DELETE/path`. + package struct Path: Sendable, Hashable { + /// - Remark: Generated from `#/paths/api/tunnels/{name}/DELETE/path/name`. + package var name: Swift.String + /// Creates a new `Path`. + /// + /// - Parameters: + /// - name: + package init(name: Swift.String) { + self.name = name + } + } + package var path: Operations.stopTunnel.Input.Path + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + package init(path: Operations.stopTunnel.Input.Path) { + self.path = path + } + } + @frozen package enum Output: Sendable, Hashable { + package struct NoContent: Sendable, Hashable { + /// Creates a new `NoContent`. + package init() {} + } + /// Tunnel stopped successfully + /// + /// - Remark: Generated from `#/paths//api/tunnels/{name}/delete(stopTunnel)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.stopTunnel.Output.NoContent) + /// The associated value of the enum case if `self` is `.noContent`. + /// + /// - Throws: An error if `self` is not `.noContent`. + /// - SeeAlso: `.noContent`. + package var noContent: Operations.stopTunnel.Output.NoContent { + get throws { + switch self { + case let .noContent(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "noContent", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + } +} diff --git a/Sources/Ngrokit/FileHandle.swift b/Sources/Ngrokit/FileHandle.swift index 5afa7c8..b21203e 100644 --- a/Sources/Ngrokit/FileHandle.swift +++ b/Sources/Ngrokit/FileHandle.swift @@ -1,25 +1,78 @@ +// +// FileHandle.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation -extension FileHandle { - func parseNgrokErrorCode() throws -> Int? { +// swiftlint:disable:next force_try +private let ngrokCLIErrorRegex = try! NSRegularExpression(pattern: "ERR_NGROK_([0-9]+)") + +/// A protocol for handling data. +public protocol DataHandle { + /// Reads data until the end. + /// + /// - Returns: The data read until the end, or `nil` if there is no more data. + /// - Throws: An error if there was a problem reading the data. + func readToEnd() throws -> Data? +} + +extension FileHandle: DataHandle {} + +extension DataHandle { + /// Parses the ngrok error code from the data. + /// + /// - Returns: The parsed ngrok error code. + /// - Throws: An error if there was a problem parsing the error code. + internal func parseNgrokErrorCode() throws -> NgrokError { guard let data = try readToEnd() else { - return nil + throw RuntimeError.unknownError } guard let text = String(data: data, encoding: .utf8) else { - throw Ngrok.CLI.RunError.invalidErrorData(data) + throw RuntimeError.invalidErrorData(data) } - guard let match = Ngrok.CLI.errorRegex.firstMatch( + guard let match = ngrokCLIErrorRegex.firstMatch( in: text, range: .init(location: 0, length: text.count) ), match.numberOfRanges > 0 else { - return nil + throw RuntimeError.unknownEarlyTermination(text) } guard let range = Range(match.range(at: 1), in: text) else { - return nil + throw RuntimeError.unknownEarlyTermination(text) + } + guard let code = Int(text[range]) else { + throw RuntimeError.unknownEarlyTermination(text) + } + guard let error = NgrokError(rawValue: code) else { + throw RuntimeError.unknownNgrokErrorCode(code) } - return Int(text[range]) + return error } } diff --git a/Sources/Ngrokit/ListTunnelsRequest.swift b/Sources/Ngrokit/ListTunnelsRequest.swift deleted file mode 100644 index b2fda65..0000000 --- a/Sources/Ngrokit/ListTunnelsRequest.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import PrchModel - -public struct ListTunnelsRequest: ServiceCall { - public typealias ServiceAPI = Ngrok.API - - public let parameters: [String: String] = [:] - - public static var requiresCredentials: Bool { - true - } - - public typealias SuccessType = NgrokTunnelResponse - - public typealias BodyType = Empty - - public init() {} - - public var method: PrchModel.RequestMethod = .GET - - public let path = "api/tunnels" - - public let queryParameters = [String: Any]() - - public let headers = [String: String]() -} diff --git a/Sources/Ngrokit/Ngrok.swift b/Sources/Ngrokit/Ngrok.swift deleted file mode 100644 index 7ee0347..0000000 --- a/Sources/Ngrokit/Ngrok.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import Prch -import PrchModel - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -public enum Ngrok { - public struct API: PrchModel.API { - public let encoder: any PrchModel.Encoder = JSONEncoder() - - public let decoder: any PrchModel.Decoder = JSONDecoder() - - public typealias DataType = Data - - public let baseURLComponents = URLComponents(string: "http://127.0.0.1:4040")! - - public let headers: [String: String] = [:] - - public static let shared: API = .init() - } - - public struct CLI { - // swiftlint:disable:next force_try - static let errorRegex = try! NSRegularExpression(pattern: "ERR_NGROK_([0-9]+)") - public init(executableURL: URL) { - self.executableURL = executableURL - } - - let executableURL: URL - - public enum RunError: Error { - case earlyTermination(Process.TerminationReason, Int?) - case invalidErrorData(Data) - } - - private func processTerminated(_: Process) {} - - public func http(port: Int, timeout: DispatchTime) async throws -> Process { - let process = Process() - let pipe = Pipe() - let semaphore = DispatchSemaphore(value: 0) - process.executableURL = executableURL - process.arguments = ["http", port.description] - process.standardError = pipe - process.terminationHandler = { _ in - semaphore.signal() - } - try process.run() - return try await withCheckedThrowingContinuation { continuation in - let semaphoreResult = semaphore.wait(timeout: timeout) - guard semaphoreResult == .success else { - process.terminationHandler = nil - continuation.resume(returning: process) - return - } - let errorCode: Int? - - do { - errorCode = try pipe.fileHandleForReading.parseNgrokErrorCode() - } catch { - continuation.resume(with: .failure(error)) - return - } - continuation.resume(with: - .failure( - RunError.earlyTermination(process.terminationReason, errorCode)) - ) - } - } - } -} diff --git a/Sources/Ngrokit/NgrokCLIAPI.swift b/Sources/Ngrokit/NgrokCLIAPI.swift new file mode 100644 index 0000000..dde9d32 --- /dev/null +++ b/Sources/Ngrokit/NgrokCLIAPI.swift @@ -0,0 +1,54 @@ +// +// NgrokCLIAPI.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +/// A protocol for interacting with the Ngrok CLI API. +/// +/// This protocol extends the `Sendable` protocol. +/// +/// - Note: The `Sendable` protocol is not defined in this code snippet. +/// +/// - Important: The `NgrokCLIAPI` protocol is not defined in this code snippet. +/// +/// - Requires: The `NgrokProcess` type to be defined. +/// +/// - SeeAlso: `NgrokProcess` +public protocol NgrokCLIAPI: Sendable { + /// Creates a process for the specified HTTP port. + /// + /// - Parameter httpPort: The port number for the HTTP server. + /// + /// - Returns: An instance of `NgrokProcess` for the specified port. + func process(forHTTPPort httpPort: Int) -> any NgrokProcess +} + +/// A type representing a process created by the Ngrok CLI API. +/// +/// - Note: The `NgrokProcess` type is not defined in this code snippet. diff --git a/Sources/Ngrokit/NgrokClient.swift b/Sources/Ngrokit/NgrokClient.swift new file mode 100644 index 0000000..749ec9d --- /dev/null +++ b/Sources/Ngrokit/NgrokClient.swift @@ -0,0 +1,120 @@ +// +// NgrokClient.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import NgrokOpenAPIClient +import OpenAPIRuntime + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +/// A client for interacting with the Ngrok API. +/// +/// Use this client to start and stop tunnels, as well as list existing tunnels. +/// +/// To create an instance of `NgrokClient`, +/// you need to provide a transport and an optional server URL. +/// +/// Example usage: +/// +/// ```swift +/// let client = NgrokClient(transport: URLSession.shared) +/// ``` +/// +/// - Note: The default server URL is `try! Servers.server1()`. +/// +/// - SeeAlso: `TunnelRequest` +/// - SeeAlso: `Tunnel` +public struct NgrokClient: Sendable { + // swiftlint:disable force_try + /// The default server URL. + public static let defaultServerURL = try! Servers.server1() + // swiftlint:enable force_try + + private let underlyingClient: any APIProtocol + + /// Initializes a new instance of `NgrokClient`. + /// + /// - Parameters: + /// - transport: The transport to use for making API requests. + /// - serverURL: The server URL to use. If `nil`, + /// the default server URL will be used. + public init(transport: any ClientTransport, serverURL: URL? = nil) { + let underlyingClient = NgrokOpenAPIClient.Client( + serverURL: serverURL ?? Self.defaultServerURL, + transport: transport + ) + self.init(underlyingClient: underlyingClient) + } + + internal init(underlyingClient: any APIProtocol) { + self.underlyingClient = underlyingClient + } + + /// Starts a new tunnel. + /// + /// - Parameter request: The tunnel request. + /// + /// - Returns: The created tunnel. + /// + /// - Throws: An error if the tunnel creation fails. + public func startTunnel(_ request: TunnelRequest) async throws -> Tunnel { + let tunnelRequest: Components.Schemas.TunnelRequest + tunnelRequest = .init(request: request) + let response = try await underlyingClient.startTunnel( + .init( + body: .json(tunnelRequest) + ) + ).created.body.json + let tunnel: Tunnel = try .init(response: response) + return tunnel + } + + /// Stops a tunnel with the specified name. + /// + /// - Parameter name: The name of the tunnel to stop. + /// + /// - Throws: An error if the tunnel cannot be stopped. + public func stopTunnel(withName name: String) async throws { + _ = try await underlyingClient.stopTunnel(path: .init(name: name)).noContent + } + + /// Lists all existing tunnels. + /// + /// - Returns: An array of tunnels. + /// + /// - Throws: An error if the tunnel listing fails. + public func listTunnels() async throws -> [Tunnel] { + try await underlyingClient + .listTunnels() + .ok.body.json.tunnels + .map(Tunnel.init(response:)) + } +} diff --git a/Sources/Ngrokit/NgrokError.swift b/Sources/Ngrokit/NgrokError.swift new file mode 100644 index 0000000..ca803ab --- /dev/null +++ b/Sources/Ngrokit/NgrokError.swift @@ -0,0 +1,108 @@ +// +// NgrokError.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +// swiftlint:disable line_length + +/// An enumeration representing possible errors that can occur with Ngrok. +/// +/// - invalidMetadataLength: The metadata length is invalid. +/// - accountLimitExceeded: The account limit for simultaneous ngrok agent sessions has been exceeded. +/// - unsupportedAgentVersion: The ngrok agent version is no longer supported. +/// - captchaFailed: The captcha solving failed. +/// - accountViolation: Creating an ngrok account is disallowed due to violation of the terms of service. +/// - gatewayError: Ngrok gateway error. +/// - tunnelNotFound: The tunnel was not found. +/// - accountBanned: The account associated with the hostname has been banned. +/// - passwordTooShort: The password is too short. +/// - accountCreationNotAllowed: Creating a new account is not allowed. +/// - invalidCredentials: The email or password entered is not valid. +/// - userAlreadyExists: A user with the email address already exists. +/// - disallowedEmailProvider: Sign-ups are disallowed for the email provider. +/// - htmlContentSignupRequired: Signing up for an ngrok account and installing the authtoken is required before serving HTML content. +/// - websiteVisitWarning: A warning before visiting a website served by ngrok.com. +/// - tunnelConnectionFailed: The ngrok agent failed to establish a connection to the upstream web service. +/// +public enum NgrokError: Int, LocalizedError { + case invalidMetadataLength = 100 + case accountLimitExceeded = 108 + case unsupportedAgentVersion = 120 + case captchaFailed = 1_205 + case accountViolation = 1_226 + case gatewayError = 3_004 + case tunnelNotFound = 3_200 + case accountBanned = 3_208 + case passwordTooShort = 4_011 + case accountCreationNotAllowed = 4_013 + case invalidCredentials = 4_100 + case userAlreadyExists = 4_101 + case disallowedEmailProvider = 4_108 + case htmlContentSignupRequired = 6_022 + case websiteVisitWarning = 6_024 + case tunnelConnectionFailed = 8_012 + + public var errorDescription: String? { + switch self { + case .invalidMetadataLength: + return "Invalid metadata length" + case .accountLimitExceeded: + return "You've hit your account limit for simultaneous ngrok agent sessions. Try stopping an existing agent or upgrading your account." + case .unsupportedAgentVersion: + return "Your ngrok agent version is no longer supported. Only the most recent version of the ngrok agent is supported without an account. Update to a newer version with ngrok update or by downloading from https://ngrok.com/download. Sign up for an account to avoid forced version upgrades: https://ngrok.com/signup." + case .captchaFailed: + return "You failed to solve the captcha, please try again." + case .accountViolation: + return "You are disallowed from creating an ngrok account due to violation of the terms of service." + case .gatewayError: + return "Ngrok gateway error. The server returned an invalid or incomplete HTTP response. Try starting ngrok with the full upstream service URL (e.g. ngrok http https://localhost:8081)" + case .tunnelNotFound: + return "Tunnel not found. This could be because your agent is not online or your tunnel has been flagged by our automated moderation system." + case .accountBanned: + return "The account associated with this hostname has been banned. We've determined this account to be in violation of ngrok's terms of service. If you are the account owner and believe this is a mistake, please contact support@ngrok.com." + case .passwordTooShort: + return "Your password must be at least 10 characters." + case .accountCreationNotAllowed: + return "You may not create a new account because you are already a member of a free account. Upgrade or delete that account first before creating a new account." + case .invalidCredentials: + return "The email or password you entered is not valid." + case .userAlreadyExists: + return "A user with the email address already exists." + case .disallowedEmailProvider: + return "Sign-ups are disallowed for the email provider. Please sign up with a different email provider." + case .htmlContentSignupRequired: + return "Before you can serve HTML content, you must sign up for an ngrok account and install your authtoken." + case .websiteVisitWarning: + return "You are about to visit HOSTPORT, served by SERVINGIP. This website is served for free through ngrok.com. You should only visit this website if you trust whoever sent the link to you." + case .tunnelConnectionFailed: + return "Traffic was successfully tunneled to the ngrok agent, but the agent failed to establish a connection to the upstream web service" + } + } + // swiftlint:enable line_length +} diff --git a/Sources/Ngrokit/NgrokMacProcess.swift b/Sources/Ngrokit/NgrokMacProcess.swift new file mode 100644 index 0000000..07cae72 --- /dev/null +++ b/Sources/Ngrokit/NgrokMacProcess.swift @@ -0,0 +1,115 @@ +// +// NgrokMacProcess.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +/// A class representing a Ngrok process on macOS. +/// +/// This class conforms to the `NgrokProcess` protocol. +/// +/// - Note: This class is an actor, +/// meaning it can be safely accessed from multiple concurrent tasks. +/// +/// - Author: Leo Dion +/// - Version: 2024 +/// - Copyright: © BrightDigit +/// +/// - SeeAlso: `NgrokProcess` +public actor NgrokMacProcess: NgrokProcess { + private var terminationHandler: (@Sendable (any Error) -> Void)? + internal let process: ProcessType + private let pipe: ProcessType.PipeType + + /// Initializes a new instance of `NgrokMacProcess`. + /// + /// - Parameters: + /// - ngrokPath: The path to the Ngrok executable. + /// - httpPort: The port to use for the HTTP connection. + /// - processType: The type of process to use. + /// + /// - Returns: A new instance of `NgrokMacProcess`. + public init( + ngrokPath: String, + httpPort: Int, + processType _: ProcessType.Type + ) { + self.init( + process: .init( + executableFilePath: ngrokPath, + scheme: "http", + port: httpPort + ) + ) + } + + private init( + process: ProcessType, + pipe: ProcessType.PipeType? = nil, + terminationHandler: (@Sendable (any Error) -> Void)? = nil + ) { + self.terminationHandler = terminationHandler + self.process = process + if let pipe { + self.pipe = pipe + } else { + let newPipe: ProcessType.PipeType = process.createPipe() + self.process.standardError = newPipe + self.pipe = newPipe + } + } + + /// A private method that handles the termination of the process. + /// + /// - Parameters: + /// - forProcess: The process that has terminated. + @Sendable + private nonisolated func terminationHandler(forProcess _: any Processable) { + Task { + let error: any Error + do { + error = try self.pipe.fileHandleForReading.parseNgrokErrorCode() + } catch let runtimeError as RuntimeError { + error = runtimeError + } + await self.terminationHandler?(error) + } + } + + /// Runs the Ngrok process. + /// + /// - Parameters: + /// - onError: A closure that handles any errors that occur during the process. + /// + /// - Throws: An error if the process fails to run. + public func run(onError: @Sendable @escaping (any Error) -> Void) async throws { + process.setTerminationHandler(terminationHandler(forProcess:)) + terminationHandler = onError + try process.run() + } +} diff --git a/Sources/Ngrokit/NgrokProcess.swift b/Sources/Ngrokit/NgrokProcess.swift new file mode 100644 index 0000000..2e3aee1 --- /dev/null +++ b/Sources/Ngrokit/NgrokProcess.swift @@ -0,0 +1,47 @@ +// +// NgrokProcess.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// A protocol representing a process for running Ngrok. +/// +/// - Note: This protocol is `Sendable`, allowing it to be used in asynchronous contexts. +/// +/// - Important: Implementations of this protocol +/// must provide a `run` method that runs the Ngrok process. +/// +/// - Parameter onError: A closure to handle any errors that occur during the process. +/// +/// - Throws: An error if the process fails to run. +/// +/// - SeeAlso: `NgrokProcessImplementation` +public protocol NgrokProcess: Sendable { + /// Runs the Ngrok process. + /// + /// - Parameter onError: A closure to handle any errors that occur during the process. + func run(onError: @Sendable @escaping (any Error) -> Void) async throws +} diff --git a/Sources/Ngrokit/NgrokProcessCLIAPI.swift b/Sources/Ngrokit/NgrokProcessCLIAPI.swift new file mode 100644 index 0000000..6d73a14 --- /dev/null +++ b/Sources/Ngrokit/NgrokProcessCLIAPI.swift @@ -0,0 +1,67 @@ +// +// NgrokProcessCLIAPI.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +/// A struct representing the Ngrok CLI API. +/// +/// Use this API to interact with Ngrok and create tunnels. +/// +/// - Note: This API requires a valid Ngrok installation. +/// +/// - Parameters: +/// - ngrokPath: The path to the Ngrok executable. +/// +/// - SeeAlso: `NgrokCLIAPI` +public struct NgrokProcessCLIAPI { + /// The path to the Ngrok executable. + public let ngrokPath: String + + /// Initializes a new instance of `NgrokProcessCLIAPI`. + /// + /// - Parameter ngrokPath: The path to the Ngrok executable. + public init(ngrokPath: String) { + self.ngrokPath = ngrokPath + } +} + +extension NgrokProcessCLIAPI: NgrokCLIAPI { + /// Creates a new `NgrokProcess` for the specified HTTP port. + /// + /// - Parameter httpPort: The port number for the HTTP server. + /// + /// - Returns: An instance of `NgrokProcess` for the specified HTTP port. + public func process(forHTTPPort httpPort: Int) -> any NgrokProcess { + NgrokMacProcess( + ngrokPath: ngrokPath, + httpPort: httpPort, + processType: ProcessType.self + ) + } +} diff --git a/Sources/Ngrokit/NgrokTunnel.swift b/Sources/Ngrokit/NgrokTunnel.swift deleted file mode 100644 index a9d03f9..0000000 --- a/Sources/Ngrokit/NgrokTunnel.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import PrchModel - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -public struct NgrokTunnel: Codable, Content { - public let name: String - // swiftlint:disable:next identifier_name - public let public_url: URL - public let config: NgrokTunnelConfiguration -} diff --git a/Sources/Ngrokit/NgrokTunnelConfiguration.swift b/Sources/Ngrokit/NgrokTunnelConfiguration.swift index d129652..6e25f7a 100644 --- a/Sources/Ngrokit/NgrokTunnelConfiguration.swift +++ b/Sources/Ngrokit/NgrokTunnelConfiguration.swift @@ -1,10 +1,50 @@ +// +// NgrokTunnelConfiguration.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// Represents the configuration for an Ngrok tunnel. +/// +/// - Note: This struct is `Sendable`. +/// +/// - Parameters: +/// - addr: The URL of the tunnel. +/// - inspect: A boolean value indicating whether to enable inspection of the tunnel. +/// import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif -public struct NgrokTunnelConfiguration: Codable { - let addr: URL - let inspect: Bool +public struct NgrokTunnelConfiguration: Sendable { + /// The URL of the tunnel. + public let addr: URL + + /// A boolean value indicating whether to enable inspection of the tunnel. + public let inspect: Bool } diff --git a/Sources/Ngrokit/NgrokTunnelRequest.swift b/Sources/Ngrokit/NgrokTunnelRequest.swift deleted file mode 100644 index cc74378..0000000 --- a/Sources/Ngrokit/NgrokTunnelRequest.swift +++ /dev/null @@ -1,17 +0,0 @@ -import PrchModel - -public struct NgrokTunnelRequest: Codable, Content { - internal init(addr: String, proto: String, name: String) { - self.addr = addr - self.proto = proto - self.name = name - } - - public init(port: Int, proto: String = "http", name: String = "vapor-development") { - self.init(addr: port.description, proto: proto, name: name) - } - - let addr: String - let proto: String - let name: String -} diff --git a/Sources/Ngrokit/NgrokTunnelResponse.swift b/Sources/Ngrokit/NgrokTunnelResponse.swift deleted file mode 100644 index 9633cc0..0000000 --- a/Sources/Ngrokit/NgrokTunnelResponse.swift +++ /dev/null @@ -1,4 +0,0 @@ -import PrchModel -public struct NgrokTunnelResponse: Content, Codable { - public let tunnels: [NgrokTunnel] -} diff --git a/Sources/Ngrokit/NgrokUrlParser.swift b/Sources/Ngrokit/NgrokUrlParser.swift deleted file mode 100644 index 58dc4cf..0000000 --- a/Sources/Ngrokit/NgrokUrlParser.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -public struct NgrokUrlParser { - public static let defaultApiURL = URL(string: "http://localhost:4040/api/tunnels")! - - public func url(fromResponse response: NgrokTunnelResponse) -> URL? { - response.tunnels.sorted { lhs, _ -> Bool in - lhs.public_url.scheme?.hasSuffix("s") == true - }.first?.public_url - } - - public init() {} -} diff --git a/Sources/Ngrokit/Pipable.swift b/Sources/Ngrokit/Pipable.swift new file mode 100644 index 0000000..c745b58 --- /dev/null +++ b/Sources/Ngrokit/Pipable.swift @@ -0,0 +1,46 @@ +// +// Pipable.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +/// A protocol for types that can be piped. +/// +/// Types conforming to this protocol must also conform to `Sendable`. +/// +/// - Note: The associated type `DataHandleType` must conform to `DataHandle`. +/// +/// - Important: The `fileHandleForReading` property +/// must be implemented to provide a handle for reading data. +public protocol Pipable: Sendable { + /// The associated type representing the data handle. + associatedtype DataHandleType: DataHandle + + /// The file handle used for reading data. + var fileHandleForReading: DataHandleType { get } +} diff --git a/Sources/Ngrokit/Processable.swift b/Sources/Ngrokit/Processable.swift new file mode 100644 index 0000000..2969dff --- /dev/null +++ b/Sources/Ngrokit/Processable.swift @@ -0,0 +1,103 @@ +// +// Processable.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// A protocol for objects that can be processed. +/// +/// - Note: This protocol is `Sendable` and `AnyObject`. +/// +/// - Important: The associated type `PipeType` must conform to `Pipable`. +/// +/// - SeeAlso: `Pipable` +/// +/// - SeeAlso: `TerminationReason` +/// +/// - SeeAlso: `PipeType` +/// +/// - SeeAlso: `run()` +/// +/// - SeeAlso: `setTerminationHandler(_:)` +/// +/// - SeeAlso: `createPipe()` +/// +/// - SeeAlso: `standardErrorPipe` +/// +/// - SeeAlso: `terminationReason` +/// +/// - Requires: `executableFilePath`, `scheme`, and `port` parameters to initialize. +/// +/// - Requires: `run()` method to be implemented. +/// +/// - Requires: `standardErrorPipe` property to be gettable and settable. +/// +/// - Requires: `terminationReason` property to be gettable. +/// +/// - Requires: `setTerminationHandler(_:)` method to be implemented. +/// +public protocol Processable: Sendable, AnyObject { + /// The associated type for the pipe used by the process. + associatedtype PipeType: Pipable + + /// The pipe used for standard error output. + var standardError: PipeType? { get set } + + /// The reason for the process termination. + var terminationReason: TerminationReason { get } + + /// Initializes a `Processable` object. + /// + /// - Parameters: + /// - executableFilePath: The file path of the executable. + /// - scheme: The scheme to use. + /// - port: The port to use. + /// + /// - Requires: This initializer must be implemented. + init(executableFilePath: String, scheme: String, port: Int) + + /// Sets a closure to be called when the process terminates. + /// + /// - Parameters: + /// - closure: The closure to be called. + /// + /// - Requires: This method must be implemented. + func setTerminationHandler(_ closure: @escaping @Sendable (Self) -> Void) + + /// Creates a new pipe. + /// + /// - Returns: A new instance of `PipeType`. + /// + /// - Requires: This method must be implemented. + func createPipe() -> PipeType + + /// Runs the process. + /// + /// - Throws: An error if the process fails. + /// + /// - Requires: This method must be implemented. + func run() throws +} diff --git a/Sources/Ngrokit/ProcessableProcess.swift b/Sources/Ngrokit/ProcessableProcess.swift new file mode 100644 index 0000000..a425807 --- /dev/null +++ b/Sources/Ngrokit/ProcessableProcess.swift @@ -0,0 +1,111 @@ +// +// ProcessableProcess.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +#if os(macOS) + + /// A process that can be processed and executed. + /// + /// - Note: This class is only available on macOS. + /// + /// - Important: Make sure to set the `standardErrorPipe` + /// property before executing the process. + /// + /// - SeeAlso: `Processable` + public final class ProcessableProcess: Processable { + /// The type of pipe used for standard error. + public typealias PipeType = Pipe + + private let process: Process + + public var terminationReason: TerminationReason { + process.terminationReason + } + + /// The pipe used for standard error. + public var standardError: Pipe? { + get { + process.standardError as? Pipe + } + set { + process.standardError = newValue + } + } + + private init(process: Process) { + self.process = process + } + + /// Initializes a new `ProcessableProcess` instance. + /// + /// - Parameters: + /// - executableFilePath: The file path of the executable. + /// - scheme: The scheme to use. + /// - port: The port to use. + /// + /// - Important: Make sure to set the `standardErrorPipe` + /// property before executing the process. + public convenience init(executableFilePath: String, scheme: String, port: Int) { + let process = Process() + process.executableURL = .init(filePath: executableFilePath) + process.arguments = [scheme, port.description] + self.init(process: process) + } + + /// Sets the termination handler closure for the process. + /// + /// - Parameter closure: The closure to be called when the process terminates. + public func setTerminationHandler( + _ closure: @escaping @Sendable (ProcessableProcess) -> Void + ) { + process.terminationHandler = { process in + guard let pprocess = process as? ProcessableProcess else { + assertionFailure() + closure(self) + return + } + closure(pprocess) + } + } + + /// Creates a new pipe. + /// + /// - Returns: A new `Pipe` instance. + public func createPipe() -> Pipe { + Pipe() + } + + public func run() throws { + try process.run() + } + } + + extension Pipe: Pipable {} +#endif diff --git a/Sources/Ngrokit/RuntimeError.swift b/Sources/Ngrokit/RuntimeError.swift new file mode 100644 index 0000000..bee17ba --- /dev/null +++ b/Sources/Ngrokit/RuntimeError.swift @@ -0,0 +1,38 @@ +// +// RuntimeError.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +public enum RuntimeError: Error { + case invalidURL(String) + case invalidErrorData(Data) + case unknownEarlyTermination(String) + case unknownError + case unknownNgrokErrorCode(Int) +} diff --git a/Sources/Ngrokit/StartTunnelRequest.swift b/Sources/Ngrokit/StartTunnelRequest.swift deleted file mode 100644 index 9d65024..0000000 --- a/Sources/Ngrokit/StartTunnelRequest.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import PrchModel - -public struct StartTunnelRequest: ServiceCall { - public typealias SuccessType = NgrokTunnel - - public typealias BodyType = NgrokTunnelRequest - - public typealias ServiceAPI = Ngrok.API - - public static var requiresCredentials: Bool { - false - } - - public init(body: NgrokTunnelRequest) { - self.body = body - } - - public var method: PrchModel.RequestMethod = .POST - - public let path: String = "api/tunnels" - - public var parameters: [String: String] = [:] - - public let headers: [String: String] = [ - "Content-Type": "application/json" - ] - - public let name: String = "" - - public let body: NgrokTunnelRequest -} diff --git a/Sources/Ngrokit/StopTunnelRequest.swift b/Sources/Ngrokit/StopTunnelRequest.swift deleted file mode 100644 index 3bc480c..0000000 --- a/Sources/Ngrokit/StopTunnelRequest.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import PrchModel - -public struct StopTunnelRequest: ServiceCall { - public typealias SuccessType = Empty - - public typealias BodyType = Empty - - public typealias ServiceAPI = Ngrok.API - - public var parameters: [String: String] { - [:] - } - - public static var requiresCredentials: Bool { - false - } - - public init(name: String) { - self.name = name - } - - public let method = RequestMethod.DELETE - - public var path: String { - "api/tunnels/\(name)" - } - - public let queryParameters = [String: Any]() - - public let headers = [String: String]() - - public let name: String -} diff --git a/Sources/Ngrokit/TerminationReason.swift b/Sources/Ngrokit/TerminationReason.swift new file mode 100644 index 0000000..dff7ba5 --- /dev/null +++ b/Sources/Ngrokit/TerminationReason.swift @@ -0,0 +1,47 @@ +// +// TerminationReason.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +// swiftlint:disable file_types_order +#if os(macOS) + /// Represents the reason for the termination of a process. + public typealias TerminationReason = Process.TerminationReason +#else + + /// Represents the reason for the termination of a process. + /// + /// - exit: The process exited normally. + /// - uncaughtSignal: The process terminated due to an uncaught signal. + public enum TerminationReason: Int, Sendable { + case exit = 1 + case uncaughtSignal = 2 + } +#endif +// swiftlint:enable file_types_order diff --git a/Sources/Ngrokit/Tunnel.swift b/Sources/Ngrokit/Tunnel.swift new file mode 100644 index 0000000..4723cd6 --- /dev/null +++ b/Sources/Ngrokit/Tunnel.swift @@ -0,0 +1,106 @@ +// +// Tunnel.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import NgrokOpenAPIClient + +/// A struct representing a tunnel. +/// +/// - Note: This struct conforms to the `Sendable` protocol. +/// +/// - Parameters: +/// - name: The name of the tunnel. +/// - publicURL: The public URL of the tunnel. +/// - config: The configuration of the tunnel. +/// +/// - SeeAlso: `NgrokTunnelConfiguration` +/// +/// - Throws: `RuntimeError.invalidURL` if the public URL or the address URL is invalid. +/// +/// - SeeAlso: `RuntimeError` +/// +/// - Note: This struct has an additional initializer +/// that takes a `TunnelResponse` object. +/// +/// - SeeAlso: `Components.Schemas.TunnelResponse` +public struct Tunnel: Sendable { + /// The name of the tunnel. + public let name: String + + /// The public URL of the tunnel. + public let publicURL: URL + + /// The configuration of the tunnel. + public let config: NgrokTunnelConfiguration + + /// Initializes a new `Tunnel` instance. + /// + /// - Parameters: + /// - name: The name of the tunnel. + /// - publicURL: The public URL of the tunnel. + /// - config: The configuration of the tunnel. + public init(name: String, publicURL: URL, config: NgrokTunnelConfiguration) { + self.name = name + self.publicURL = publicURL + self.config = config + } +} + +extension Tunnel { + /// Initializes a new `Tunnel` instance from a `TunnelResponse` object. + /// + /// - Parameters: + /// - response: The `TunnelResponse` object. + /// + /// - Throws: `RuntimeError.invalidURL` + /// if the public URL or the address URL is invalid. + /// + /// - SeeAlso: `RuntimeError` + /// + /// - Note: This initializer is internal and should not be used directly. + /// + /// - SeeAlso: `Components.Schemas.TunnelResponse` + + internal init(response: Components.Schemas.TunnelResponse) throws { + guard let publicURL = URL(string: response.public_url) else { + throw RuntimeError.invalidURL(response.public_url) + } + guard let addr = URL(string: response.config.addr) else { + throw RuntimeError.invalidURL(response.config.addr) + } + self.init( + name: response.name, + publicURL: publicURL, + config: .init( + addr: addr, + inspect: response.config.inspect + ) + ) + } +} diff --git a/Sources/Ngrokit/TunnelRequest.swift b/Sources/Ngrokit/TunnelRequest.swift new file mode 100644 index 0000000..1255f11 --- /dev/null +++ b/Sources/Ngrokit/TunnelRequest.swift @@ -0,0 +1,74 @@ +// +// TunnelRequest.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import NgrokOpenAPIClient + +/// Represents a request to create a tunnel. +public struct TunnelRequest: Sendable { + /// The address of the tunnel. + public let addr: String + + /// The protocol to use for the tunnel. + public let proto: String + + /// The name of the tunnel. + public let name: String + + /// Initializes a new `TunnelRequest` instance. + /// + /// - Parameters: + /// - addr: The address of the tunnel. + /// - proto: The protocol to use for the tunnel. + /// - name: The name of the tunnel. + public init(addr: String, proto: String, name: String) { + self.addr = addr + self.proto = proto + self.name = name + } + + /// Initializes a new `TunnelRequest` instance. + /// + /// - Parameters: + /// - port: The port number of the tunnel. + /// - name: The name of the tunnel. + /// - proto: The protocol to use for the tunnel. Default value is "http". + public init(port: Int, name: String, proto: String = "http") { + self.init(addr: port.description, proto: proto, name: name) + } +} + +extension Components.Schemas.TunnelRequest { + /// Initializes a new `Components.Schemas.TunnelRequest` instance. + /// + /// - Parameters: + /// - request: The `TunnelRequest` instance to initialize from. + internal init(request: TunnelRequest) { + self.init(addr: request.addr, proto: request.proto, name: request.name) + } +} diff --git a/Sources/NgrokitMocks/MockAPI.swift b/Sources/NgrokitMocks/MockAPI.swift new file mode 100644 index 0000000..2b91ea9 --- /dev/null +++ b/Sources/NgrokitMocks/MockAPI.swift @@ -0,0 +1,92 @@ +// +// MockAPI.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Ngrokit +import NgrokOpenAPIClient + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +package final actor MockAPI: APIProtocol { + private let actualStopTunnelResult: Result? + package private(set) var stopTunnelPassed: [Operations.stopTunnel.Input] = [] + + private let actualStartTunnelResult: Result? + package private(set) var startTunnelPassed: [Operations.startTunnel.Input] = [] + + private let actualListTunnelResult: Result? + package private(set) var listTunnelPassed: [Operations.listTunnels.Input] = [] + + package init( + actualStopTunnelResult: Result? = nil, + actualStartTunnelResult: Result? = nil, + actualListTunnelResult: Result? = nil + ) { + self.actualStopTunnelResult = actualStopTunnelResult + self.actualStartTunnelResult = actualStartTunnelResult + self.actualListTunnelResult = actualListTunnelResult + } + + // swiftlint:disable unavailable_function force_unwrapping + package func getTunnel( + _: NgrokOpenAPIClient.Operations.getTunnel.Input + ) async throws -> NgrokOpenAPIClient.Operations.getTunnel.Output { + fatalError("not implemented") + } + + package func stopTunnel( + _ input: Operations.stopTunnel.Input + ) async throws -> Operations.stopTunnel.Output { + stopTunnelPassed.append(input) + return try actualStopTunnelResult!.get() + } + + package func startTunnel( + _ input: Operations.startTunnel.Input + ) async throws -> Operations.startTunnel.Output { + startTunnelPassed.append(input) + return try actualStartTunnelResult!.get() + } + + package func listTunnels( + _ input: NgrokOpenAPIClient.Operations.listTunnels.Input + ) async throws -> NgrokOpenAPIClient.Operations.listTunnels.Output { + listTunnelPassed.append(input) + return try actualListTunnelResult!.get() + } + + package func get_sol_api( + _: NgrokOpenAPIClient.Operations.get_sol_api.Input + ) async throws -> NgrokOpenAPIClient.Operations.get_sol_api.Output { + fatalError("not implemented") + } + // swiftlint:enable unavailable_function force_unwrapping +} diff --git a/Sources/NgrokitMocks/MockDataHandle.swift b/Sources/NgrokitMocks/MockDataHandle.swift new file mode 100644 index 0000000..ed89fe8 --- /dev/null +++ b/Sources/NgrokitMocks/MockDataHandle.swift @@ -0,0 +1,61 @@ +// +// MockDataHandle.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Ngrokit + +package struct MockDataHandle: DataHandle { + // swiftlint:disable line_length + package static let code: Data? = """ + ERROR: authentication failed: Your account is limited to 1 simultaneous ngrok agent session. + ERROR: You can run multiple tunnels on a single agent session using a configuration file. + ERROR: To learn more, see https://ngrok.com/docs/secure-tunnels/ngrok-agent/reference/config/ + ERROR: + ERROR: Active ngrok agent sessions in region 'us': + ERROR: - ts_2bjiyVxWh6dMoaZUfjXNsHWFNta (2607:fb90:8da8:5b15:900b:13fd:c5e7:f9c6) + ERROR: + ERROR: ERR_NGROK_108 + ERROR: + """.data(using: .utf8) + // swiftlint:enable line_length + + private let actualResult: Result + + package init(_ actualResult: Result) { + self.actualResult = actualResult + } + + package static func withNgrokCode() -> MockDataHandle { + .init(.success(code)) + } + + package func readToEnd() throws -> Data? { + try actualResult.get() + } +} diff --git a/Sources/NgrokitMocks/MockNgrokCLIAPI.swift b/Sources/NgrokitMocks/MockNgrokCLIAPI.swift new file mode 100644 index 0000000..9080351 --- /dev/null +++ b/Sources/NgrokitMocks/MockNgrokCLIAPI.swift @@ -0,0 +1,48 @@ +// +// MockNgrokCLIAPI.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Ngrokit + +package final class MockNgrokCLIAPI: NgrokCLIAPI { + package let process: any NgrokProcess + package private(set) var httpPorts = [Int]() + + package convenience init(id: UUID) { + self.init(process: MockNgrokProcess(id: id)) + } + + internal init(process: any NgrokProcess) { + self.process = process + } + + package func process(forHTTPPort _: Int) -> any Ngrokit.NgrokProcess { + process + } +} diff --git a/Sources/NgrokitMocks/MockNgrokProcess.swift b/Sources/NgrokitMocks/MockNgrokProcess.swift new file mode 100644 index 0000000..5016487 --- /dev/null +++ b/Sources/NgrokitMocks/MockNgrokProcess.swift @@ -0,0 +1,41 @@ +// +// MockNgrokProcess.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Ngrokit + +package final class MockNgrokProcess: NgrokProcess { + package let id: UUID + + package init(id: UUID) { + self.id = id + } + + package func run(onError _: @escaping @Sendable (any Error) -> Void) async throws {} +} diff --git a/Sources/NgrokitMocks/MockPipe.swift b/Sources/NgrokitMocks/MockPipe.swift new file mode 100644 index 0000000..da7186d --- /dev/null +++ b/Sources/NgrokitMocks/MockPipe.swift @@ -0,0 +1,41 @@ +// +// MockPipe.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Ngrokit + +package final class MockPipe: Pipable { + package typealias DataHandleType = MockDataHandle + + package let fileHandleForReading: MockDataHandle + + internal init(fileHandleForReading: MockDataHandle) { + self.fileHandleForReading = fileHandleForReading + } +} diff --git a/Sources/NgrokitMocks/MockProcess.swift b/Sources/NgrokitMocks/MockProcess.swift new file mode 100644 index 0000000..8b38601 --- /dev/null +++ b/Sources/NgrokitMocks/MockProcess.swift @@ -0,0 +1,92 @@ +// +// MockProcess.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Ngrokit + +package final class MockProcess: Processable { + package typealias PipeType = MockPipe + + package let executableFilePath: String + package let scheme: String + package let port: Int + package let pipeDataResult: Result + package let runError: (any Error)? + package let terminationReason: Ngrokit.TerminationReason + package var standardError: MockPipe? + + package private(set) var isTerminationHandlerSet = false + package private(set) var isRunCalled = false + + internal init( + executableFilePath: String, + scheme: String, + port: Int, + terminationReason: TerminationReason, + standardError: MockPipe? = nil, + pipeDataResult: Result = .success(nil), + runError: (any Error)? = nil + ) { + self.executableFilePath = executableFilePath + self.scheme = scheme + self.port = port + self.standardError = standardError + self.terminationReason = terminationReason + self.pipeDataResult = pipeDataResult + self.runError = runError + } + + package convenience init( + executableFilePath: String, + scheme: String, + port: Int + ) { + self.init( + executableFilePath: executableFilePath, + scheme: scheme, + port: port, + terminationReason: .exit + ) + } + + package nonisolated func createPipe() -> MockPipe { + .init(fileHandleForReading: .init(pipeDataResult)) + } + + package func setTerminationHandler(_: @escaping @Sendable (MockProcess) -> Void) { + isTerminationHandlerSet = true + } + + package func run() throws { + isRunCalled = true + if let error = runError { + throw error + } + } +} diff --git a/Sources/NgrokitMocks/URL.swift b/Sources/NgrokitMocks/URL.swift new file mode 100644 index 0000000..71240a6 --- /dev/null +++ b/Sources/NgrokitMocks/URL.swift @@ -0,0 +1,40 @@ +// +// URL.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +extension URL { + public static func temporaryDirectory() -> URL { + URL(fileURLWithPath: NSTemporaryDirectory()) + } +} diff --git a/Sources/Sublimation/AnyKVdbTunnelClient.swift b/Sources/Sublimation/AnyKVdbTunnelClient.swift deleted file mode 100644 index 2471447..0000000 --- a/Sources/Sublimation/AnyKVdbTunnelClient.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -@available(*, deprecated) -public struct AnyKVdbTunnelClient: KVdbTunnelClient { - private init( - client: Any, - getValue: @escaping (Key, String) async throws -> URL, - saveValue: @escaping (URL, Key, String) async throws -> Void - ) { - self.client = client - getValueClosure = getValue - saveValueClosure = saveValue - } - - public init( - client: TunnelClientType - ) where TunnelClientType.Key == Self.Key { - self.init( - client: client, - getValue: client.getValue(ofKey:fromBucket:), - saveValue: client.saveValue(_:withKey:inBucket:) - ) - } - - let client: Any - let getValueClosure: (Key, String) async throws -> URL - let saveValueClosure: (URL, Key, String) async throws -> Void - - public func getValue( - ofKey key: Key, - fromBucket bucketName: String - ) async throws -> URL { - try await getValueClosure(key, bucketName) - } - - public func saveValue( - _ value: URL, - withKey key: Key, - inBucket bucketName: String - ) async throws { - try await saveValueClosure(value, key, bucketName) - } -} diff --git a/Sources/Sublimation/KVdb.swift b/Sources/Sublimation/KVdb.swift index 979a9f9..bea1a51 100644 --- a/Sources/Sublimation/KVdb.swift +++ b/Sources/Sublimation/KVdb.swift @@ -1,28 +1,96 @@ +// +// KVdb.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif +/// A utility class for interacting with KVdb. +/// +/// KVdb is a key-value database service. +/// +/// - Note: This class requires the `Foundation` framework. +/// +/// - SeeAlso: [KVdb](https://kvdb.io/) +/// +/// - Author: Leo Dion +/// +/// - Version: 2024 +/// +/// - License: MIT License public enum KVdb { + /// The base URL string for KVdb. public static let baseString = "https://kvdb.io/" - public static func path(forKey key: Key, atBucket bucketName: String) -> String { - "\(bucketName)/\(key)" + /// Constructs the path for a given key in a bucket. + /// + /// - Parameters: + /// - key: The key for the value. + /// - bucketName: The name of the bucket. + /// + /// - Returns: The constructed path. + public static func path(forKey key: some Any, atBucket bucketName: String) -> String { + "/\(bucketName)/\(key)" } - public static func construct( + /// Constructs a URL for a given key in a bucket. + /// + /// - Parameters: + /// - URLType: The type of URL to construct. + /// - key: The key for the value. + /// - bucketName: The name of the bucket. + /// + /// - Returns: The constructed URL. + public static func construct( _: URLType.Type, - forKey key: Key, + forKey key: some Any, atBucket bucketName: String ) -> URLType { URLType( - kvDBBase: Self.baseString, - keyBucketPath: Self.path(forKey: key, atBucket: bucketName) + kvDBBase: baseString, + keyBucketPath: path(forKey: key, atBucket: bucketName) ) } - public static func url( + /// Retrieves the URL for a given key in a bucket. + /// + /// - Parameters: + /// - key: The key for the value. + /// - bucketName: The name of the bucket. + /// - session: The URLSession to use for the request. Defaults to `.ephemeral`. + /// + /// - Returns: The URL for the key, or `nil` if it doesn't exist. + /// + /// - Throws: An error if the request fails. + public static func url( withKey key: Key, atBucket bucketName: String, using session: URLSession = .ephemeral() diff --git a/Sources/Sublimation/KVdbTunnelClient.swift b/Sources/Sublimation/KVdbTunnelClient.swift index 02a9165..76ce653 100644 --- a/Sources/Sublimation/KVdbTunnelClient.swift +++ b/Sources/Sublimation/KVdbTunnelClient.swift @@ -1,17 +1,71 @@ +// +// KVdbTunnelClient.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif -public protocol KVdbTunnelClient { - associatedtype Key +/// A client for interacting with KVdb Tunnel. +/// +/// - Note: This client conforms to the `Sendable` protocol. +/// +/// - Note: The `Key` type must also conform to the `Sendable` protocol. +/// +/// - Warning: This client is not thread-safe. +/// +/// - Important: This client requires the `FoundationNetworking` module to be imported. +/// +/// - SeeAlso: `KVdbTunnelClientProtocol` +public protocol KVdbTunnelClient: Sendable { + /// The type of key used to access values in the KVdb Tunnel. + associatedtype Key: Sendable + + /// Retrieves the value associated with the specified key from the specified bucket. + /// + /// - Parameters: + /// - key: The key used to access the value. + /// - bucketName: The name of the bucket. + /// + /// - Returns: The URL of the retrieved value. + /// + /// - Throws: An error if the value cannot be retrieved. func getValue(ofKey key: Key, fromBucket bucketName: String) async throws -> URL - func saveValue(_ value: URL, withKey key: Key, inBucket bucketName: String) async throws -} -extension KVdbTunnelClient { - public func eraseToAnyClient() -> AnyKVdbTunnelClient { - AnyKVdbTunnelClient(client: self) - } + /// Saves the specified value with the specified key in the specified bucket. + /// + /// - Parameters: + /// - value: The URL of the value to be saved. + /// - key: The key used to access the value. + /// - bucketName: The name of the bucket. + /// + /// - Throws: An error if the value cannot be saved. + func saveValue(_ value: URL, withKey key: Key, inBucket bucketName: String) async throws } diff --git a/Sources/Sublimation/KVdbTunnelRepository.swift b/Sources/Sublimation/KVdbTunnelRepository.swift index 8e52147..7730778 100644 --- a/Sources/Sublimation/KVdbTunnelRepository.swift +++ b/Sources/Sublimation/KVdbTunnelRepository.swift @@ -1,48 +1,51 @@ +// +// KVdbTunnelRepository.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif -public class KVdbTunnelRepository: WritableTunnelRepository { - internal init(client: AnyKVdbTunnelClient? = nil, bucketName: String) { +public final class KVdbTunnelRepository: WritableTunnelRepository { + private let client: any KVdbTunnelClient + private let bucketName: String + public init(client: any KVdbTunnelClient, bucketName: String) { self.client = client self.bucketName = bucketName } - public init(bucketName: String) { - client = nil - self.bucketName = bucketName - } - - public init( - client: TunnelClientType, - bucketName: String - ) where TunnelClientType.Key == Key { - self.client = client.eraseToAnyClient() - self.bucketName = bucketName - } - - var client: AnyKVdbTunnelClient? - let bucketName: String - - public func setupClient( - _ client: TunnelClientType - ) where TunnelClientType.Key == Key { - self.client = client.eraseToAnyClient() - } - public func tunnel(forKey key: Key) async throws -> URL? { - guard let client = self.client else { - preconditionFailure() - } - return try await client.getValue(ofKey: key, fromBucket: bucketName) + try await client.getValue(ofKey: key, fromBucket: bucketName) } public func saveURL(_ url: URL, withKey key: Key) async throws { - guard let client = self.client else { - preconditionFailure() - } try await client.saveValue(url, withKey: key, inBucket: bucketName) } } diff --git a/Sources/Sublimation/KVdbTunnelRepositoryFactory.swift b/Sources/Sublimation/KVdbTunnelRepositoryFactory.swift new file mode 100644 index 0000000..379d816 --- /dev/null +++ b/Sources/Sublimation/KVdbTunnelRepositoryFactory.swift @@ -0,0 +1,63 @@ +// +// KVdbTunnelRepositoryFactory.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +/// This factory is used to set up and configure a +/// `KVdbTunnelRepository` with a specific bucket name. +public struct KVdbTunnelRepositoryFactory: + WritableTunnelRepositoryFactory { + /// The type of tunnel repository created by this factory. + public typealias TunnelRepositoryType = KVdbTunnelRepository + + /// The name of the bucket to use. + public let bucketName: String + + /// Initializes a new instance of the factory with the specified bucket name. + /// + /// - Parameter bucketName: The name of the bucket to use. + public init(bucketName: String) { + self.bucketName = bucketName + } + + /// Sets up a client and returns a new `KVdbTunnelRepository` instance. + /// + /// - Parameter client: The tunnel client to use. + /// - Returns: A new `KVdbTunnelRepository` instance. + public func setupClient( + _ client: TunnelClientType + ) -> KVdbTunnelRepository + where TunnelClientType: KVdbTunnelClient, TunnelClientType.Key == Key { + .init(client: client, bucketName: bucketName) + } +} diff --git a/Sources/Sublimation/KVdbURLConstructable.swift b/Sources/Sublimation/KVdbURLConstructable.swift index c7917b3..c27276d 100644 --- a/Sources/Sublimation/KVdbURLConstructable.swift +++ b/Sources/Sublimation/KVdbURLConstructable.swift @@ -1,4 +1,56 @@ +// +// KVdbURLConstructable.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation + +/// A protocol for constructing URLs for interacting with a key-value database. +/// +/// Conforming types should provide an initializer +/// that takes a base URL and a key bucket path. +/// +/// Example usage: +/// ``` +/// struct MyKVdbURLConstructor: KVdbURLConstructable { +/// init(kvDBBase: String, keyBucketPath: String) { +/// // Implementation details +/// } +/// } +/// ``` +/// +/// - SeeAlso: `KVdbURLConstructor` public protocol KVdbURLConstructable { + /// Initializes a URL constructor with the given base URL and key bucket path. + /// + /// - Parameters: + /// - kvDBBase: The base URL of the key-value database. + /// - keyBucketPath: The path to the key bucket. + /// + /// - Returns: An instance of the conforming type. init(kvDBBase: String, keyBucketPath: String) } diff --git a/Sources/Sublimation/NgrokServerError.swift b/Sources/Sublimation/NgrokServerError.swift index ffb8893..a54f613 100644 --- a/Sources/Sublimation/NgrokServerError.swift +++ b/Sources/Sublimation/NgrokServerError.swift @@ -1,4 +1,41 @@ +// +// NgrokServerError.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation + +/// An error type representing various errors that can occur +/// when working with Ngrok server. +/// +/// - clientNotSetup: The Ngrok client is not properly set up. +/// - noTunnelFound: No tunnel was found. +/// - invalidURL: The URL is invalid. +/// - cantSaveTunnel: Unable to save the tunnel with the given ID and data. public enum NgrokServerError: Error { case clientNotSetup case noTunnelFound diff --git a/Sources/Sublimation/Optional.swift b/Sources/Sublimation/Optional.swift new file mode 100644 index 0000000..e1d1be8 --- /dev/null +++ b/Sources/Sublimation/Optional.swift @@ -0,0 +1,43 @@ +// +// Optional.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +extension Optional { + /// Returns a tuple containing the wrapped value + /// of the optional and another optional value. + /// + /// - Parameter other: Another optional value. + /// + /// - Returns: A tuple containing the wrapped value of the optional and `other`, + /// or `nil` if either the optional or `other` is `nil`. + internal func flatTuple(_ other: OtherType?) -> (Wrapped, OtherType)? { + flatMap { wrapped in + other.map { (wrapped, $0) } + } + } +} diff --git a/Sources/Sublimation/Result.swift b/Sources/Sublimation/Result.swift new file mode 100644 index 0000000..d79dc96 --- /dev/null +++ b/Sources/Sublimation/Result.swift @@ -0,0 +1,42 @@ +// +// Result.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +extension Result { + internal struct EmptyError: Error {} + + internal init(success: Success?, failure: Failure?) where Failure == any Error { + if let failure { + self = .failure(failure) + } else if let success { + self = .success(success) + } else { + self = .failure(EmptyError()) + } + } +} diff --git a/Sources/Sublimation/TunnelRepository.swift b/Sources/Sublimation/TunnelRepository.swift index 61ea19f..7134168 100644 --- a/Sources/Sublimation/TunnelRepository.swift +++ b/Sources/Sublimation/TunnelRepository.swift @@ -1,6 +1,56 @@ +// +// TunnelRepository.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation -public protocol TunnelRepository { - associatedtype Key +/// A repository for managing tunnels. +/// This protocol defines the basic functionality for +/// retrieving tunnels based on a given key. +/// +/// - Note: The `Key` type must conform to the `Sendable` protocol. +/// +/// - Important: This protocol inherits from the `Sendable` protocol. +/// +/// - Warning: The `Key` associated type must also conform to the `Sendable` protocol. +/// +/// - Requires: The `Key` associated type to be specified. +public protocol TunnelRepository: Sendable { + /// The type of key used to retrieve tunnels. + associatedtype Key: Sendable + + /// Retrieves a tunnel for the specified key. + /// + /// - Parameter key: The key used to retrieve the tunnel. + /// + /// - Throws: An error if the tunnel cannot be retrieved. + /// + /// - Returns: The URL of the retrieved tunnel, if available. + func tunnel(forKey key: Key) async throws -> URL? } diff --git a/Sources/Sublimation/TunnelRepositoryFactory.swift b/Sources/Sublimation/TunnelRepositoryFactory.swift new file mode 100644 index 0000000..df1b9b3 --- /dev/null +++ b/Sources/Sublimation/TunnelRepositoryFactory.swift @@ -0,0 +1,69 @@ +// +// TunnelRepositoryFactory.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +/// A factory for creating tunnel repositories. +/// +/// The factory is responsible for +/// setting up the client and returning a tunnel repository. +/// +/// - Note: The factory must be `Sendable`. +/// +/// - Note: The associated type `TunnelRepositoryType` must conform to `TunnelRepository`. +/// +/// - Note: The factory must implement the `setupClient` method, +/// which takes a `TunnelClientType` and returns a `TunnelRepositoryType`. +/// +/// - Warning: The factory may require the `FoundationNetworking` module to be imported. +/// +/// - SeeAlso: `TunnelRepository` +/// - SeeAlso: `KVdbTunnelClient` +public protocol TunnelRepositoryFactory: Sendable { + /// The type of tunnel repository created by the factory. + associatedtype TunnelRepositoryType: TunnelRepository + + /// Sets up the client and returns a tunnel repository. + /// + /// - Parameter client: The tunnel client to use. + /// + /// - Returns: A tunnel repository. + /// + /// - Throws: An error if the setup fails. + /// + /// - Note: The `TunnelClientType` must have a `Key` type + /// that matches the `Key` type of the `TunnelRepositoryType`. + func setupClient( + _ client: TunnelClientType + ) -> TunnelRepositoryType where TunnelClientType.Key == TunnelRepositoryType.Key +} diff --git a/Sources/Sublimation/URL.swift b/Sources/Sublimation/URL.swift index 5f06ae6..72baef9 100644 --- a/Sources/Sublimation/URL.swift +++ b/Sources/Sublimation/URL.swift @@ -1,11 +1,58 @@ +// +// URL.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif +/// A type representing a URL. +/// +/// - Note: This type is an extension of `URL` and conforms to `KVdbURLConstructable`. +/// +/// - SeeAlso: `KVdbURLConstructable` extension URL: KVdbURLConstructable { + /// Initializes a `URL` instance with the given KVDB base and key bucket path. + /// + /// - Parameters: + /// - kvDBBase: The base URL of the KVDB. + /// - keyBucketPath: The path to the key bucket. + /// + /// - Note: This initializer is only available if `FoundationNetworking` is imported. + /// + /// - Precondition: `kvDBBase` must be a valid URL. + /// + /// - Postcondition: The resulting `URL` instance is constructed + /// by appending `keyBucketPath` to `kvDBBase`. public init(kvDBBase: String, keyBucketPath: String) { + // swiftlint:disable:next force_unwrapping self = URL(string: kvDBBase)!.appendingPathComponent(keyBucketPath) } } diff --git a/Sources/Sublimation/URLSession.swift b/Sources/Sublimation/URLSession.swift new file mode 100644 index 0000000..7a3d19e --- /dev/null +++ b/Sources/Sublimation/URLSession.swift @@ -0,0 +1,87 @@ +// +// URLSession.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +/// An extension to `URLSession` that provides asynchronous data fetching methods. +extension URLSession { + /// Creates a new ephemeral `URLSession` instance. + /// + /// - Returns: A new ephemeral `URLSession` instance. + public static func ephemeral() -> URLSession { + URLSession(configuration: .ephemeral) + } + + /// Fetches data asynchronously for the given request. + /// + /// - Parameter request: The request to fetch data for. + /// + /// - Returns: A tuple containing the fetched data and the URL response. + /// + /// - Throws: An error if the data fetching operation fails. + internal func dataAsync(for request: URLRequest) async throws -> (Data, URLResponse) { + #if !canImport(FoundationNetworking) + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return try await self.data(for: request) + } + #endif + + return try await withCheckedThrowingContinuation { continuation in + let task = self.dataTask(with: request) { data, response, error in + continuation.resume( + with: .init( + success: data.flatTuple(response), + failure: error + ) + ) + } + task.resume() + } + } + + /// Fetches data asynchronously from the given URL. + /// + /// - Parameter url: The URL to fetch data from. + /// + /// - Returns: A tuple containing the fetched data and the URL response. + /// + /// - Throws: An error if the data fetching operation fails. + internal func dataAsync(from url: URL) async throws -> (Data, URLResponse) { + #if !canImport(FoundationNetworking) + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + return try await data(for: .init(url: url)) + } + #endif + return try await dataAsync(for: .init(url: url)) + } +} diff --git a/Sources/Sublimation/URLSessionClient.swift b/Sources/Sublimation/URLSessionClient.swift index 39c2f88..4dbbdb6 100644 --- a/Sources/Sublimation/URLSessionClient.swift +++ b/Sources/Sublimation/URLSessionClient.swift @@ -1,72 +1,68 @@ +// +// URLSessionClient.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif -extension Result { - init(success: Success?, failure: Failure?) where Failure == Error { - if let failure = failure { - self = .failure(failure) - } else if let success = success { - self = .success(success) - } else { - self = .failure(EmptyError()) - } - } - - struct EmptyError: Error {} -} - -extension Optional { - func flatTuple(_ other: OtherType?) -> (Wrapped, OtherType)? { - flatMap { wrapped in - other.map { (wrapped, $0) } - } - } -} - -extension URLSession { - public static func ephemeral() -> URLSession { - URLSession(configuration: .ephemeral) - } - - func dataAsync(for request: URLRequest) async throws -> (Data, URLResponse) { - #if !canImport(FoundationNetworking) - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return try await self.data(for: request) - } - #endif - - return try await withCheckedThrowingContinuation { continuation in - let task = self.dataTask(with: request) { data, response, error in - continuation.resume( - with: .init( - success: data.flatTuple(response), - failure: error - ) - ) - } - task.resume() - } - } - - func dataAsync(from url: URL) async throws -> (Data, URLResponse) { - #if !canImport(FoundationNetworking) - if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { - return try await data(for: .init(url: url)) - } - #endif - return try await dataAsync(for: .init(url: url)) - } -} +/// A client for interacting with a KVdb tunnel using URLSession. +/// +/// This client conforms to the `KVdbTunnelClient` protocol. +/// +/// - Note: This client requires the `FoundationNetworking` module to be imported. +/// +/// - Warning: The `saveValue(_:withKey:inBucket:)` method will throw +/// a `NgrokServerError` if the save operation fails. +/// +/// - SeeAlso: `KVdbTunnelClient` +public struct URLSessionClient: KVdbTunnelClient { + private let session: URLSession -public struct URLSessionClient: KVdbTunnelClient { + /// Initializes a new `URLSessionClient` with the specified session. + /// + /// - Parameter session: The URLSession to use for network requests. + /// Defaults to an ephemeral session. public init(session: URLSession = .ephemeral()) { self.session = session } - let session: URLSession + /// Retrieves the value associated with a key from a specific bucket. + /// + /// - Parameters: + /// - key: The key to retrieve the value for. + /// - bucketName: The name of the bucket to retrieve the value from. + /// + /// - Returns: The URL value associated with the key. + /// + /// - Throws: A `NgrokServerError` if the retrieval operation fails. public func getValue( ofKey key: Key, fromBucket bucketName: String @@ -82,6 +78,14 @@ public struct URLSessionClient: KVdbTunnelClient { return url } + /// Saves a URL value with a specified key in a specific bucket. + /// + /// - Parameters: + /// - value: The URL value to save. + /// - key: The key to associate with the value. + /// - bucketName: The name of the bucket to save the value in. + /// + /// - Throws: A `NgrokServerError` if the save operation fails. public func saveValue( _ value: URL, withKey key: Key, diff --git a/Sources/Sublimation/WritableTunnelRepository.swift b/Sources/Sublimation/WritableTunnelRepository.swift index 650cb1b..664f5aa 100644 --- a/Sources/Sublimation/WritableTunnelRepository.swift +++ b/Sources/Sublimation/WritableTunnelRepository.swift @@ -1,14 +1,52 @@ -import Foundation +// +// WritableTunnelRepository.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif +import Foundation -public protocol WritableTunnelRepository: TunnelRepository { - func setupClient< - TunnelClientType: KVdbTunnelClient - >( - _ client: TunnelClientType - ) where TunnelClientType.Key == Self.Key +/// A repository for managing writable tunnels. +/// +/// This protocol extends the `TunnelRepository` protocol +/// and adds the ability to save a URL with a key. +/// +/// - Note: The `Key` type parameter +/// represents the type of key used to identify the tunnels. +/// +/// - SeeAlso: `TunnelRepository` +public protocol WritableTunnelRepository: TunnelRepository { + /// Saves a URL with a key. + /// + /// - Parameters: + /// - url: The URL to save. + /// - key: The key to associate with the URL. + /// + /// - Throws: An error if the save operation fails. + /// + /// - Note: This method is asynchronous. func saveURL(_ url: URL, withKey key: Key) async throws } diff --git a/Sources/Sublimation/WritableTunnelRepositoryFactory.swift b/Sources/Sublimation/WritableTunnelRepositoryFactory.swift new file mode 100644 index 0000000..f9ed470 --- /dev/null +++ b/Sources/Sublimation/WritableTunnelRepositoryFactory.swift @@ -0,0 +1,47 @@ +// +// WritableTunnelRepositoryFactory.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +/// A factory protocol for creating writable tunnel repositories. +/// +/// This protocol extends the `TunnelRepositoryFactory` protocol +/// and requires the associated `TunnelRepositoryType` +/// to conform to the `WritableTunnelRepository` protocol. +/// +/// - Note: This protocol is part of the `Sublimation` framework. +/// +/// - SeeAlso: `TunnelRepositoryFactory` +/// - SeeAlso: `WritableTunnelRepository` +public protocol WritableTunnelRepositoryFactory: TunnelRepositoryFactory + where TunnelRepositoryType: WritableTunnelRepository {} diff --git a/Sources/SublimationMocks/MockError.swift b/Sources/SublimationMocks/MockError.swift new file mode 100644 index 0000000..09aa909 --- /dev/null +++ b/Sources/SublimationMocks/MockError.swift @@ -0,0 +1,52 @@ +// +// MockError.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +package enum MockError: Error { + case value(T) +} + +extension Result { + public var error: (any Error)? { + guard case let .failure(failure) = self else { + return nil + } + return failure + } + + public func mockErrorValue() -> T? { + guard let mockError = error as? MockError else { + return nil + } + + switch mockError { + case let .value(value): + return value + } + } +} diff --git a/Sources/SublimationMocks/MockTunnelClient.swift b/Sources/SublimationMocks/MockTunnelClient.swift new file mode 100644 index 0000000..12365b4 --- /dev/null +++ b/Sources/SublimationMocks/MockTunnelClient.swift @@ -0,0 +1,76 @@ +// +// MockTunnelClient.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Sublimation + +package actor MockTunnelClient: KVdbTunnelClient { + package struct GetParameters { + package let key: Key + package let bucketName: String + } + + package struct SaveParameters { + package let value: URL + package let key: Key + package let bucketName: String + } + + internal let getValueResult: Result? + internal let saveValueError: (any Error)? + + package private(set) var getValuesPassed = [GetParameters]() + package private(set) var saveValuesPassed = [SaveParameters]() + package init( + getValueResult: Result? = nil, saveValueError: (any Error)? = nil + ) { + self.getValueResult = getValueResult + self.saveValueError = saveValueError + } + + package func getValue( + ofKey key: Key, + fromBucket bucketName: String + ) async throws -> URL { + getValuesPassed.append(.init(key: key, bucketName: bucketName)) + // swiftlint:disable:next force_unwrapping + return try getValueResult!.get() + } + + package func saveValue( + _ value: URL, + withKey key: Key, + inBucket bucketName: String + ) async throws { + saveValuesPassed.append(.init(value: value, key: key, bucketName: bucketName)) + if let saveValueError { + throw saveValueError + } + } +} diff --git a/Sources/SublimationMocks/MockURL.swift b/Sources/SublimationMocks/MockURL.swift new file mode 100644 index 0000000..8f98712 --- /dev/null +++ b/Sources/SublimationMocks/MockURL.swift @@ -0,0 +1,39 @@ +// +// MockURL.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Sublimation + +package struct MockURL: KVdbURLConstructable { + package let kvDBBase: String + package let keyBucketPath: String + package init(kvDBBase: String, keyBucketPath: String) { + self.kvDBBase = kvDBBase + self.keyBucketPath = keyBucketPath + } +} diff --git a/Sources/SublimationMocks/URL.swift b/Sources/SublimationMocks/URL.swift new file mode 100644 index 0000000..450251b --- /dev/null +++ b/Sources/SublimationMocks/URL.swift @@ -0,0 +1,40 @@ +// +// URL.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +extension URL { + public static func random() -> URL { + URL(fileURLWithPath: NSTemporaryDirectory()) + } +} diff --git a/Sources/SublimationVapor/NetworkResult.swift b/Sources/SublimationVapor/NetworkResult.swift new file mode 100644 index 0000000..0ac52b6 --- /dev/null +++ b/Sources/SublimationVapor/NetworkResult.swift @@ -0,0 +1,97 @@ +// +// NetworkResult.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import AsyncHTTPClient +import Foundation +import OpenAPIRuntime + +/// Represents the result of a network operation. +/// +/// - success: The operation was successful and contains the result value. +/// - connectionRefused: The connection was refused by the server. +/// - failure: The operation failed with an error. +/// +/// - Note: This type is internal and should not be used outside of the framework. +internal enum NetworkResult { + case success(T) + case connectionRefused(ClientError) + case failure(any Error) +} + +extension NetworkResult { + internal init(error: any Error) { + guard let error = error as? ClientError else { + self = .failure(error) + return + } + + #if canImport(Network) + if let posixError = error.underlyingError as? HTTPClient.NWPOSIXError { + guard posixError.errorCode == .ECONNREFUSED else { + self = .failure(error) + return + } + self = .connectionRefused(error) + return + } + #endif + + if let clientError = error.underlyingError as? HTTPClientError { + guard clientError == .connectTimeout else { + self = .failure(error) + return + } + self = .connectionRefused(error) + return + } + + self = .failure(error) + } + + internal init(_ closure: @escaping () async throws -> T) async { + do { + self = try await .success(closure()) + } catch { + self = .init(error: error) + } + } + + internal func get() throws -> T? { + switch self { + case .connectionRefused: + return nil + + case let .failure(error): + throw error + + case let .success(item): + return item + } + } +} diff --git a/Sources/SublimationVapor/NgrokCLIAPIConfiguration.swift b/Sources/SublimationVapor/NgrokCLIAPIConfiguration.swift new file mode 100644 index 0000000..78e4383 --- /dev/null +++ b/Sources/SublimationVapor/NgrokCLIAPIConfiguration.swift @@ -0,0 +1,71 @@ +// +// NgrokCLIAPIConfiguration.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Logging +import Vapor + +/// Configuration for the Ngrok CLI API server. +/// +/// - Note: This configuration conforms to `NgrokServerConfiguration` protocol. +/// +/// - Note: This configuration conforms to `NgrokVaporConfiguration` protocol. +/// +/// - SeeAlso: `NgrokCLIAPIServer` +public struct NgrokCLIAPIConfiguration: NgrokServerConfiguration { + /// The type of server to use. + public typealias Server = NgrokCLIAPIServer + + /// The port number to run the server on. + public let port: Int + + /// The logger to use for logging. + public let logger: Logger +} + +extension NgrokCLIAPIConfiguration: NgrokVaporConfiguration { + /// Initializes a new instance of + /// `NgrokCLIAPIConfiguration` using a `ServerApplication`. + /// + /// - Parameter serverApplication: The server application to use for configuration. + internal init(serverApplication: any ServerApplication) { + self.init( + port: serverApplication.httpServerConfigurationPort, + logger: serverApplication.logger + ) + } + + /// Initializes a new instance of `NgrokCLIAPIConfiguration` + /// using a `Vapor.Application`. + /// + /// - Parameter application: The Vapor application to use for configuration. + + public init(application: Vapor.Application) { + self.init(serverApplication: application) + } +} diff --git a/Sources/SublimationVapor/NgrokCLIAPIServer.swift b/Sources/SublimationVapor/NgrokCLIAPIServer.swift index ca704d7..30af28e 100644 --- a/Sources/SublimationVapor/NgrokCLIAPIServer.swift +++ b/Sources/SublimationVapor/NgrokCLIAPIServer.swift @@ -1,208 +1,217 @@ +// +// NgrokCLIAPIServer.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation +import Logging import Ngrokit - -import Prch -import PrchModel -// import class Prch.Client -import PrchVapor -import Vapor -#if canImport(FoundationNetworking) - import FoundationNetworking - // swiftlint:disable:next identifier_name - private let NSEC_PER_SEC: UInt64 = 1_000_000_000 -#endif - -enum NgrokDefaults { - public static let defaultBaseURLComponents = - URLComponents(string: "http://127.0.0.1:4040")! -} - -protocol NgrokServiceProtocol: ServiceProtocol where ServiceAPI == Ngrok.API {} - -class NgrokService: Service, NgrokServiceProtocol - where SessionType.ResponseType.DataType == Ngrok.API.ResponseDataType, - SessionType.RequestDataType == Ngrok.API.RequestDataType { - internal init(session: SessionType) { - self.session = session +import OpenAPIRuntime + +/// A server implementation for Ngrok CLI API. +/// +/// - Note: This server conforms to the `NgrokServer` and `Sendable` protocols. +/// +/// - SeeAlso: `NgrokServer` +/// - SeeAlso: `Sendable` +public struct NgrokCLIAPIServer: NgrokServer, Sendable { + private enum TunnelAttemptResult { + case network(NetworkResult) + case error(ClientError) } - let session: SessionType - - var authorizationManager: any SessionAuthenticationManager { - NullAuthorizationManager() + private struct TunnelResult { + let isOld: Bool + let tunnel: Tunnel } - typealias API = Ngrok.API - - var api: Ngrok.API { - Ngrok.API.shared - } -} - -class NgrokCLIAPIServer: NgrokServer { - internal init( - cli: Ngrok.CLI, - prchClient: (any NgrokServiceProtocol)? = nil, - port: Int? = nil, - logger: Logger? = nil, - ngrokProcess: Process? = nil, - clientSearchTimeoutNanoseconds: UInt64 = NSEC_PER_SEC / 5, - cliProcessTimeout: DispatchTimeInterval = .seconds(2), - delegate: NgrokServerDelegate? = nil + /// The delegate for the server. + internal let delegate: any NgrokServerDelegate + + /// The client for interacting with Ngrok. + private let client: NgrokClient + + /// The process for running Ngrok. + internal let process: any NgrokProcess + + /// The port number to use. + internal let port: Int + + /// The logger for logging server events. + internal let logger: Logger + + /// Initializes a new instance of `NgrokCLIAPIServer`. + /// + /// - Parameters: + /// - delegate: The delegate for the server. + /// - client: The client for interacting with Ngrok. + /// - process: The process for running Ngrok. + /// - port: The port number to use. + /// - logger: The logger for logging server events. + public init( + delegate: any NgrokServerDelegate, + client: NgrokClient, + process: any NgrokProcess, + port: Int, + logger: Logger ) { - self.cli = cli - prchClientMember = prchClient + self.delegate = delegate + self.client = client + self.process = process self.port = port self.logger = logger - self.ngrokProcess = ngrokProcess - self.cliProcessTimeout = cliProcessTimeout - self.clientSearchTimeoutNanoseconds = clientSearchTimeoutNanoseconds - self.delegate = delegate } - public convenience init( - ngrokPath: String, - prchClient: (any NgrokServiceProtocol)? = nil, - port: Int? = nil, - logger: Logger? = nil, - ngrokProcess: Process? = nil, - delegate: NgrokServerDelegate? = nil - ) { - self.init( - cli: .init(executableURL: .init(fileURLWithPath: ngrokPath)), - prchClient: prchClient, - port: port, - logger: logger, - ngrokProcess: ngrokProcess, - delegate: delegate - ) - } + private static func attemptTunnel( + withClient client: NgrokClient + ) async -> TunnelAttemptResult { + let networkResult = await NetworkResult { + try await client.listTunnels().first + } + switch networkResult { + case let .connectionRefused(error): + return .error(error) - let cli: Ngrok.CLI - let clientSearchTimeoutNanoseconds: UInt64 - let cliProcessTimeout: DispatchTimeInterval - var prchClientMember: (any NgrokServiceProtocol)? - var port: Int? - var logger: Logger! - var ngrokProcess: Process? { - didSet { - self.ngrokProcess?.terminationHandler = self.ngrokProcessTerminated + default: + return .network(networkResult) } } - weak var delegate: NgrokServerDelegate? - - func setupLogger(_ logger: Logger) { - self.logger = logger - } + private static func searchForCreatedTunnel( + withClient client: NgrokClient, + within timeout: TimeInterval, + logger: Logger + ) async throws -> Tunnel? { + let start = Date() + var networkResult: NetworkResult? + var lastError: ClientError? + var attempts = 0 + while networkResult == nil, (-start.timeIntervalSinceNow) < timeout { + logger.debug("Attempt #\(attempts + 1)") + try await Task.sleep(for: .seconds(5), tolerance: .seconds(5)) + let result = await attemptTunnel(withClient: client) + attempts += 1 + switch result { + case let .network(newNetworkResult): + networkResult = newNetworkResult + + case let .error(error): + lastError = error + } + } - func ngrokProcessTerminated(_: Process) { - guard let port = self.port else { - return + if let lastError, networkResult == nil { + logger.error("Timeout Occured After \(-start.timeIntervalSinceNow) seconds.") + throw lastError } - startHttpTunnel(port: port) + return try networkResult?.get()?.flatMap { $0 } } - var prchClient: any NgrokServiceProtocol { - guard let client = prchClientMember else { - fatalError() - } - return client + /// Handles a CLI error. + /// + /// - Parameter error: The error that occurred. + @Sendable + private func cliError(_ error: any Error) { + delegate.server(self, errorDidOccur: error) } - func setupClient(_ client: Vapor.Client) { - let service = NgrokService(session: SessionClient(client: client)) - prchClientMember = service - } + private func searchForExistingTunnel( + within timeout: TimeInterval + ) async throws -> TunnelResult? { + logger.debug("Starting Search for Existing Tunnel") - public enum TunnelError: Error { - case noTunnelCreated - } + let result = await NetworkResult { + try await client.listTunnels().first + } - func startHttpTunnel(port: Int) { - Task { - let tunnel: NgrokTunnel - do { - tunnel = try await self.startHttp(port: port) - } catch { - self.delegate?.server(self, failedWithError: error) - return - } - self.delegate?.server(self, updatedTunnel: tunnel) + switch result { + case .connectionRefused: + logger.notice( + "Ngrok not running. Waiting for Process and New Tunnel... (about 30 secs)" + ) + try await process.run(onError: cliError(_:)) + + case let .success(tunnel): + logger.debug("Process Already Running.") + return tunnel.map { .init(isOld: true, tunnel: $0) } + + case let .failure(error): + throw error } - } - public func waitForTaskCompletion( - withTimeoutInNanoseconds timeout: UInt64, - _ task: @escaping () async -> R - ) async -> R? { - await withTaskGroup(of: R?.self) { group in - await withUnsafeContinuation { continuation in - group.addTask { - continuation.resume() - return await task() - } - } - group.addTask { - await Task.yield() - try? await Task.sleep(nanoseconds: timeout) - return nil - } - defer { group.cancelAll() } - return await group.next()! + return try await Self.searchForCreatedTunnel( + withClient: client, + within: timeout, + logger: logger + ) + .map { + .init(isOld: false, tunnel: $0) } } - func startHttp(port: Int) async throws -> NgrokTunnel { - self.port = port - logger.debug("Starting Ngrok Tunnel...") - let tunnels: [NgrokTunnel] - - let result = await waitForTaskCompletion( - withTimeoutInNanoseconds: clientSearchTimeoutNanoseconds - ) { - try? await self.prchClient.request(ListTunnelsRequest()).tunnels - }?.flatMap { $0 } - - if let firstCallTunnels = result { - tunnels = firstCallTunnels - } else { - do { - logger.debug("Starting New Ngrok Client") - let ngrokProcess = try await cli.http( - port: port, - timeout: .now() + cliProcessTimeout - ) - guard let tunnel = try await prchClient.request( - ListTunnelsRequest() - ).tunnels.first else { - ngrokProcess.terminate() - throw TunnelError.noTunnelCreated - } - self.ngrokProcess = ngrokProcess - logger.debug("Created Ngrok Process...") - return tunnel - } catch let Ngrok.CLI.RunError.earlyTermination(_, errorCode) - where errorCode == 108 { - logger.debug("Ngrok Process Already Created.") - } catch { - logger.debug("Error thrown: \(error.localizedDescription)") - throw error + private func newTunnel() async throws -> Tunnel { + if let tunnel = try await searchForExistingTunnel(within: 60.0) { + if tunnel.isOld { + try await client.stopTunnel(withName: tunnel.tunnel.name) + logger.info("Existing Tunnel Stopped. \(tunnel.tunnel.publicURL)") + } else { + return tunnel.tunnel } - - logger.debug("Listing Tunnels") - tunnels = try await prchClient.request(ListTunnelsRequest()).tunnels } - if let oldTunnel = tunnels.first { - logger.debug("Deleting Existing Tunnel: \(oldTunnel.public_url) ") - try await prchClient.request(StopTunnelRequest(name: oldTunnel.name)) + return try await client.startTunnel( + .init( + port: port, + name: "vapor-development" + ) + ) + } + + /// Runs the server. + public func run() async { + let start = Date() + let newTunnel: Tunnel + do { + newTunnel = try await self.newTunnel() + } catch { + delegate.server(self, errorDidOccur: error) + return } + let seconds = Int(-start.timeIntervalSinceNow) + logger.notice("New Tunnel Created in \(seconds) secs: \(newTunnel.publicURL)") - logger.debug("Creating Tunnel...") - let tunnel = try await prchClient.request(StartTunnelRequest(body: .init(port: port))) + delegate.server(self, updatedTunnel: newTunnel) + } - return tunnel + /// Starts the server. + public func start() { + Task { + await run() + } } } diff --git a/Sources/SublimationVapor/NgrokCLIAPIServerFactory.swift b/Sources/SublimationVapor/NgrokCLIAPIServerFactory.swift new file mode 100644 index 0000000..8ec6c80 --- /dev/null +++ b/Sources/SublimationVapor/NgrokCLIAPIServerFactory.swift @@ -0,0 +1,104 @@ +// +// NgrokCLIAPIServerFactory.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import Ngrokit +import NIOCore +import OpenAPIAsyncHTTPClient + +/// A factory for creating Ngrok CLI API servers. +/// +/// This factory conforms to the `NgrokServerFactory` protocol. +/// +/// - Note: This factory requires the `NgrokCLIAPI` type to be `Processable`. +/// +/// - SeeAlso: `NgrokServerFactory` +public struct NgrokCLIAPIServerFactory: NgrokServerFactory { + /// The configuration type for the Ngrok CLI API server. + public typealias Configuration = NgrokCLIAPIConfiguration + + /// The Ngrok CLI API instance. + private let cliAPI: any NgrokCLIAPI + + /// The timeout duration for API requests. + private let timeout: TimeAmount + + /// Initializes a new instance of `NgrokCLIAPIServerFactory`. + /// + /// - Parameters: + /// - cliAPI: The Ngrok CLI API instance. + /// - timeout: The timeout duration for API requests. Default is 1 second. + public init( + cliAPI: any NgrokCLIAPI, + timeout: TimeAmount = .seconds(1) + ) { + self.cliAPI = cliAPI + self.timeout = timeout + } + + /// Initializes a new instance of `NgrokCLIAPIServerFactory` + /// with the specified Ngrok path. + /// + /// - Parameters: + /// - ngrokPath: The path to the Ngrok executable. + /// - timeout: The timeout duration for API requests. Default is 1 second. + + public init(ngrokPath: String, timeout: TimeAmount = .seconds(1)) { + self.init( + cliAPI: NgrokProcessCLIAPI(ngrokPath: ngrokPath), + timeout: timeout + ) + } + + /// Creates a new Ngrok CLI API server. + /// + /// - Parameters: + /// - configuration: The configuration for the server. + /// - handler: The delegate for the server. + /// + /// - Returns: A new `NgrokCLIAPIServer` instance. + + public func server( + from configuration: Configuration, + handler: any NgrokServerDelegate + ) -> NgrokCLIAPIServer { + let client = NgrokClient( + transport: AsyncHTTPClientTransport(configuration: .init(timeout: timeout)) + ) + + let process = cliAPI.process(forHTTPPort: configuration.port) + return .init( + delegate: handler, + client: client, + process: process, + port: configuration.port, + logger: configuration.logger + ) + } +} diff --git a/Sources/SublimationVapor/NgrokServer.swift b/Sources/SublimationVapor/NgrokServer.swift index 300e7db..b1b0e60 100644 --- a/Sources/SublimationVapor/NgrokServer.swift +++ b/Sources/SublimationVapor/NgrokServer.swift @@ -1,21 +1,43 @@ -import Foundation -import Vapor -public protocol NgrokServer: AnyObject { - func startHttpTunnel(port: Int) - func setupClient(_ client: Vapor.Client) - func setupLogger(_ logger: Logger) - var delegate: NgrokServerDelegate? { get set } -} +// +// NgrokServer.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// -extension NgrokServer { - func startTunnelFor( - application: Application, - withDelegate delegate: NgrokServerDelegate - ) { - self.delegate = delegate - setupClient(application.client) - setupLogger(application.logger) - let port = application.http.server.shared.configuration.port - startHttpTunnel(port: port) - } +/// A protocol for starting a Ngrok server. +/// +/// Implement this protocol to start a Ngrok server. +/// +/// - Note: The Ngrok server allows you to expose a local server to the internet. +/// +/// - Important: Make sure to call the `start()` method to start the Ngrok server. +public protocol NgrokServer { + /// Starts the Ngrok server. + /// + /// Call this method to start the Ngrok server and + /// expose your local server to the internet. + func start() } diff --git a/Sources/SublimationVapor/NgrokServerConfiguration.swift b/Sources/SublimationVapor/NgrokServerConfiguration.swift new file mode 100644 index 0000000..01a8e51 --- /dev/null +++ b/Sources/SublimationVapor/NgrokServerConfiguration.swift @@ -0,0 +1,36 @@ +// +// NgrokServerConfiguration.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// A protocol that defines the configuration for an Ngrok server. +/// +/// - Note: The associated type `Server` must conform to the `NgrokServer` protocol. +public protocol NgrokServerConfiguration { + /// Server for starting an `ngrok` process. + associatedtype Server: NgrokServer +} diff --git a/Sources/SublimationVapor/NgrokServerDelegate.swift b/Sources/SublimationVapor/NgrokServerDelegate.swift index 3a096e0..c22c2d3 100644 --- a/Sources/SublimationVapor/NgrokServerDelegate.swift +++ b/Sources/SublimationVapor/NgrokServerDelegate.swift @@ -1,7 +1,51 @@ +// +// NgrokServerDelegate.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Ngrokit -public protocol NgrokServerDelegate: AnyObject { - func server(_ server: NgrokServer, updatedTunnel tunnel: NgrokTunnel) - func server(_ server: NgrokServer, errorDidOccur error: Error) - func server(_ server: NgrokServer, failedWithError error: Error) +/// A delegate protocol for `NgrokServer` that handles server events and errors. +public protocol NgrokServerDelegate: AnyObject, Sendable { + /// Notifies the delegate that a tunnel has been updated. + /// + /// - Parameters: + /// - server: The `NgrokServer` instance that triggered the event. + /// - tunnel: The updated `Tunnel` object. + /// + /// - Note: This method is called whenever a tunnel's status or configuration changes. + func server(_ server: any NgrokServer, updatedTunnel tunnel: Tunnel) + + /// Notifies the delegate that an error has occurred. + /// + /// - Parameters: + /// - server: The `NgrokServer` instance that triggered the event. + /// - error: The error that occurred. + /// + /// - Note: This method is called whenever an error occurs during server operations. + func server(_ server: any NgrokServer, errorDidOccur error: any Error) } diff --git a/Sources/SublimationVapor/NgrokServerError.swift b/Sources/SublimationVapor/NgrokServerError.swift new file mode 100644 index 0000000..7c15b5c --- /dev/null +++ b/Sources/SublimationVapor/NgrokServerError.swift @@ -0,0 +1,50 @@ +// +// NgrokServerError.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Sublimation +import Vapor + +/// An error that can occur when interacting with the Ngrok server. +/// +/// - Note: This error is specific to the Sublimation framework. +/// +/// - SeeAlso: `NgrokServerError.cantSaveTunnel(_:)` +extension NgrokServerError { + /// Creates a `NgrokServerError` instance representing a failure to save a tunnel. + /// + /// - Parameters: + /// - response: The client response that triggered the error. + /// + /// - Returns: A `NgrokServerError` instance with the appropriate error details. + internal static func cantSaveTunnel(_ response: ClientResponse) -> NgrokServerError { + let code = Int(response.status.code) + let data = response.body.map { Data(buffer: $0, byteTransferStrategy: .automatic) } + return .cantSaveTunnel(code, data) + } +} diff --git a/Sources/SublimationVapor/NgrokServerFactory.swift b/Sources/SublimationVapor/NgrokServerFactory.swift new file mode 100644 index 0000000..17f9680 --- /dev/null +++ b/Sources/SublimationVapor/NgrokServerFactory.swift @@ -0,0 +1,46 @@ +// +// NgrokServerFactory.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +/// A factory protocol for creating Ngrok servers. +public protocol NgrokServerFactory: Sendable { + /// The associated type representing the configuration for the server. + associatedtype Configuration: NgrokServerConfiguration + + /// Creates a server instance based on the provided configuration. + /// + /// - Parameters: + /// - configuration: The configuration for the server. + /// - handler: The delegate object that handles server events. + /// + /// - Returns: A server instance based on the provided configuration. + func server( + from configuration: Configuration, + handler: any NgrokServerDelegate + ) -> Configuration.Server +} diff --git a/Sources/SublimationVapor/NgrokVaporConfiguration.swift b/Sources/SublimationVapor/NgrokVaporConfiguration.swift new file mode 100644 index 0000000..ac2c55c --- /dev/null +++ b/Sources/SublimationVapor/NgrokVaporConfiguration.swift @@ -0,0 +1,56 @@ +// +// NgrokVaporConfiguration.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Vapor + +/// A protocol that defines the configuration for Ngrok in a Vapor application. +/// +/// This protocol inherits from `NgrokServerConfiguration`. +/// +/// To conform to this protocol, implement the `init(application:)` initializer. +/// +/// Example usage: +/// ``` +/// struct MyNgrokConfiguration: NgrokVaporConfiguration { +/// init(application: Application) { +/// // Configure Ngrok settings here +/// } +/// } +/// ``` +/// +/// - Note: This protocol is public. +public protocol NgrokVaporConfiguration: NgrokServerConfiguration { + /// Initializes a new instance of the configuration. + /// + /// - Parameter application: The Vapor application. + /// + /// - Note: This initializer is required to conform to + /// the `NgrokVaporConfiguration` protocol. + init(application: Application) +} diff --git a/Sources/SublimationVapor/ServerApplication.swift b/Sources/SublimationVapor/ServerApplication.swift new file mode 100644 index 0000000..c561221 --- /dev/null +++ b/Sources/SublimationVapor/ServerApplication.swift @@ -0,0 +1,47 @@ +// +// ServerApplication.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Logging +import Vapor + +/// A protocol that defines the server application. +internal protocol ServerApplication { + /// The port number for the HTTP server configuration. + var httpServerConfigurationPort: Int { get } + + /// The logger for the server application. + var logger: Logger { get } +} + +extension Vapor.Application: ServerApplication { + /// The port number for the HTTP server configuration. + internal var httpServerConfigurationPort: Int { + http.server.configuration.port + } +} diff --git a/Sources/SublimationVapor/SublimationLifecycleHandler.swift b/Sources/SublimationVapor/SublimationLifecycleHandler.swift index 62c800d..e0ed5ee 100644 --- a/Sources/SublimationVapor/SublimationLifecycleHandler.swift +++ b/Sources/SublimationVapor/SublimationLifecycleHandler.swift @@ -1,4 +1,36 @@ +// +// SublimationLifecycleHandler.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import AsyncHTTPClient import Ngrokit +import OpenAPIAsyncHTTPClient +import OpenAPIRuntime import Sublimation import Vapor @@ -6,68 +38,209 @@ import Vapor import FoundationNetworking #endif -public class SublimationLifecycleHandler< - TunnelRepositoryType: WritableTunnelRepository ->: LifecycleHandler, NgrokServerDelegate { - public func server(_: NgrokServer, updatedTunnel tunnel: Ngrokit.NgrokTunnel) { - Task { - do { - try await self.tunnelRepo.saveURL(tunnel.public_url, withKey: self.key) - } catch { - self.logger?.error( - "Unable to save url to repository: \(error.localizedDescription)" - ) - return - } - self.logger?.notice( - "Saved url \(tunnel.public_url) to repository with key \(self.key)" - ) - } - } - - public func server(_: NgrokServer, errorDidOccur _: Error) {} +/// A handler for managing the lifecycle of the Sublimation application. +/// +/// - Note: This handler is responsible for starting and stopping the Ngrok server, +/// as well as saving and handling tunnels. +/// +/// - Important: This handler is designed to work with a specific configuration of +/// `WritableTunnelRepositoryFactoryType` and `NgrokServerFactoryType`. +/// +/// - Parameters: +/// - WritableTunnelRepositoryFactoryType: +/// A factory type for creating a writable tunnel repository. +/// - NgrokServerFactoryType: A factory type for creating an Ngrok server. +/// +/// - SeeAlso: `NgrokServerDelegate` +public actor SublimationLifecycleHandler< + WritableTunnelRepositoryFactoryType: WritableTunnelRepositoryFactory, + NgrokServerFactoryType: NgrokServerFactory +>: LifecycleHandler, NgrokServerDelegate + where NgrokServerFactoryType.Configuration: NgrokVaporConfiguration { + private let factory: NgrokServerFactoryType + private let repoFactory: WritableTunnelRepositoryFactoryType + private let key: WritableTunnelRepositoryFactoryType.TunnelRepositoryType.Key - public func server(_: NgrokServer, failedWithError _: Error) {} + private var tunnelRepo: WritableTunnelRepositoryFactoryType.TunnelRepositoryType? + private var logger: Logger? + private var server: (any NgrokServer)? + /// Initializes the Sublimation lifecycle handler. + /// + /// - Parameters: + /// - factory: The factory for creating an Ngrok server. + /// - repoFactory: The factory for creating a writable tunnel repository. + /// - key: The key for the tunnel repository. public init( - server: NgrokServer, - repo: TunnelRepositoryType, - key: TunnelRepositoryType.Key + factory: NgrokServerFactoryType, + repoFactory: WritableTunnelRepositoryFactoryType, + key: WritableTunnelRepositoryFactoryType.TunnelRepositoryType.Key ) { - self.server = server - tunnelRepo = repo + self.init( + factory: factory, + repoFactory: repoFactory, + key: key, + tunnelRepo: nil, + logger: nil, + server: nil + ) + } + + private init( + factory: NgrokServerFactoryType, + repoFactory: WritableTunnelRepositoryFactoryType, + key: WritableTunnelRepositoryFactoryType.TunnelRepositoryType.Key, + tunnelRepo: WritableTunnelRepositoryFactoryType.TunnelRepositoryType?, + logger: Logger?, + server: (any NgrokServer)? + ) { + self.factory = factory + self.repoFactory = repoFactory self.key = key + self.tunnelRepo = tunnelRepo + self.logger = logger + self.server = server + } + + /// Saves the tunnel URL to the tunnel repository. + /// + /// - Parameters: + /// - tunnel: The tunnel to save. + /// + /// - Note: This method is asynchronous. + /// + /// - SeeAlso: `Tunnel` + private func saveTunnel(_ tunnel: Tunnel) async { + do { + try await tunnelRepo?.saveURL(tunnel.publicURL, withKey: key) + } catch { + logger?.error( + "Unable to save url to repository: \(error.localizedDescription)" + ) + return + } + logger?.notice( + "Saved url \(tunnel.publicURL) to repository with key \(key)" + ) + } + + /// Handles an error that occurred during tunnel operation. + /// + /// - Parameters: + /// - error: The error that occurred. + /// + /// - Note: This method is asynchronous. + private func onError(_ error: any Error) async { + logger?.error("Error running tunnel: \(error.localizedDescription)") } - let server: NgrokServer - let tunnelRepo: TunnelRepositoryType - let key: TunnelRepositoryType.Key - var logger: Logger? + /// Called when an Ngrok server updates a tunnel. + /// + /// - Parameters: + /// - server: The Ngrok server. + /// - tunnel: The updated tunnel. + /// + /// - Note: This method is nonisolated. + /// + /// - SeeAlso: `NgrokServer` + /// - SeeAlso: `Tunnel` + public nonisolated func server(_: any NgrokServer, updatedTunnel tunnel: Tunnel) { + Task { + await self.saveTunnel(tunnel) + } + } + + /// Called when an error occurs in the Ngrok server. + /// + /// - Parameters: + /// - server: The Ngrok server. + /// - error: The error that occurred. + /// + /// - Note: This method is nonisolated. + /// + /// - SeeAlso: `NgrokServer` + public nonisolated func server(_: any NgrokServer, errorDidOccur error: any Error) { + Task { + await self.onError(error) + } + } - public func didBoot(_ application: Application) throws { + /// Begins the Sublimation application from the given application. + /// + /// - Parameters: + /// - application: The Vapor application. + /// + /// - Note: This method is private and asynchronous. + /// + /// - SeeAlso: `Application` + private func beginFromApplication(_ application: Application) async { + let server = factory.server( + from: NgrokServerFactoryType.Configuration(application: application), + handler: self + ) logger = application.logger - server.startTunnelFor(application: application, withDelegate: self) - tunnelRepo.setupClient( + tunnelRepo = repoFactory.setupClient( VaporTunnelClient( client: application.client, - keyType: TunnelRepositoryType.Key.self - ).eraseToAnyClient() + keyType: WritableTunnelRepositoryFactoryType.TunnelRepositoryType.Key.self + ) ) + self.server = server + server.start() } - public func shutdown(_: Application) {} + /// Called when the application is about to boot. + /// + /// - Parameters: + /// - application: The Vapor application. + /// + /// - Throws: An error if the application fails to begin. + /// + /// - Note: This method is nonisolated. + /// + /// - SeeAlso: `Application` + public nonisolated func willBoot(_ application: Application) throws { + Task { + await self.beginFromApplication(application) + } + } + + /// Called when the application is shutting down. + /// + /// - Parameters: + /// - application: The Vapor application. + /// + /// - Note: This method is nonisolated. + /// + /// - SeeAlso: `Application` + public nonisolated func shutdown(_: Application) {} } -extension SublimationLifecycleHandler { - public convenience init( - ngrokPath: String, - bucketName: String, - key: Key - ) where TunnelRepositoryType == KVdbTunnelRepository { - self.init( - server: NgrokCLIAPIServer(ngrokPath: ngrokPath), - repo: .init(bucketName: bucketName), - key: key - ) +#if os(macOS) + extension SublimationLifecycleHandler { + /// Initializes the Sublimation lifecycle handler with default values for macOS. + /// + /// - Parameters: + /// - ngrokPath: The path to the Ngrok executable. + /// - bucketName: The name of the bucket for the tunnel repository. + /// - key: The key for the tunnel repository. + /// + /// - Note: This initializer is only available on macOS. + /// + /// - SeeAlso: `KVdbTunnelRepositoryFactory` + /// - SeeAlso: `NgrokCLIAPIServerFactory` + public init( + ngrokPath: String, + bucketName: String, + key: Key + ) where WritableTunnelRepositoryFactoryType == KVdbTunnelRepositoryFactory, + NgrokServerFactoryType == NgrokCLIAPIServerFactory, + WritableTunnelRepositoryFactoryType.TunnelRepositoryType.Key == Key { + self.init( + factory: NgrokCLIAPIServerFactory(ngrokPath: ngrokPath), + repoFactory: KVdbTunnelRepositoryFactory(bucketName: bucketName), + key: key + ) + } } -} +#endif diff --git a/Sources/SublimationVapor/URI.swift b/Sources/SublimationVapor/URI.swift index 50d011e..5dd8d0a 100644 --- a/Sources/SublimationVapor/URI.swift +++ b/Sources/SublimationVapor/URI.swift @@ -1,8 +1,49 @@ +// +// URI.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation import Sublimation import Vapor +/// A type representing a Uniform Resource Identifier (URI). +/// +/// - Note: This type conforms to `KVdbURLConstructable` protocol. +/// +/// - SeeAlso: `KVdbURLConstructable` extension URI: KVdbURLConstructable { + /// Initializes a URI with the given KVDB base and key bucket path. + /// + /// - Parameters: + /// - kvDBBase: The base URL of the KVDB. + /// - keyBucketPath: The path to the key bucket. + /// + /// - Returns: A new URI instance. public init(kvDBBase: String, keyBucketPath: String) { self.init(string: kvDBBase) path = keyBucketPath diff --git a/Sources/SublimationVapor/VaporTunnelClient.swift b/Sources/SublimationVapor/VaporTunnelClient.swift index 39a4241..f922a65 100644 --- a/Sources/SublimationVapor/VaporTunnelClient.swift +++ b/Sources/SublimationVapor/VaporTunnelClient.swift @@ -1,3 +1,32 @@ +// +// VaporTunnelClient.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation import Sublimation import Vapor @@ -6,52 +35,78 @@ import Vapor import FoundationNetworking #endif -public struct VaporTunnelClient: KVdbTunnelClient { - internal init(client: Vapor.Client, keyType _: Key.Type) { +/// A client for interacting with the VaporTunnel service. +/// +/// This client conforms to the `KVdbTunnelClient` protocol. +/// +/// - Note: This client requires the Vapor framework. +/// +/// - Warning: This client is only compatible with Swift 5.5 or later. +/// +/// - Important: Make sure to import the necessary dependencies before using this client. +/// +/// - SeeAlso: `KVdbTunnelClient` +public struct VaporTunnelClient: KVdbTunnelClient { + private let client: any Vapor.Client + + /// Initializes a new instance of the `VaporTunnelClient`. + /// + /// - Parameter client: The Vapor client to use for making requests. + /// - Parameter keyType: The type of the key used for accessing values in the tunnel. + /// + /// - Returns: A new instance of `VaporTunnelClient`. + public init(client: any Vapor.Client, keyType _: Key.Type) { self.client = client } - let client: Vapor.Client - + /// Retrieves the value associated with a key from a specific bucket. + /// + /// - Parameter key: The key used to access the value. + /// - Parameter bucketName: The name of the bucket where the value is stored. + /// + /// - Throws: `NgrokServerError.invalidURL` if the retrieved URL is invalid. + /// + /// - Returns: The URL associated with the key in the specified bucket. public func getValue( ofKey key: Key, fromBucket bucketName: String ) async throws -> URL { let uri = KVdb.construct(URI.self, forKey: key, atBucket: bucketName) let url: URL? - if #available(macOS 12, *) { - url = try await client.get(uri) - .body - .map(String.init(buffer:)) - .flatMap(URL.init(string:)) - } else { - url = try await client - .get(uri) - .map { - $0.body.map(String.init(buffer:)).flatMap(URL.init(string:)) - }.get() - } + url = try await client.get(uri) + .body + .map(String.init(buffer:)) + .flatMap(URL.init(string:)) - guard let url = url else { + guard let url else { throw NgrokServerError.invalidURL } return url } + /// Saves a value with a key in a specific bucket. + /// + /// - Parameter value: The URL value to save. + /// - Parameter key: The key used to associate the value. + /// - Parameter bucketName: The name of the bucket where the value will be stored. + /// + /// - Throws: `NgrokServerError.cantSaveTunnel` if the tunnel cannot be saved. public func saveValue( _ value: URL, withKey key: Key, inBucket bucketName: String ) async throws { let uri = KVdb.construct(URI.self, forKey: key, atBucket: bucketName) - let response = try await client.post(uri, beforeSend: { request in + + let response = try await client.post(uri) { request in request.body = .init(string: value.absoluteString) - }).get() + } + .get() - if response.statusCode / 100 == 2 { + if response.status.code / 100 == 2 { return } - throw NgrokServerError.cantSaveTunnel(response.statusCode, response.data) + throw NgrokServerError.cantSaveTunnel(response) } } diff --git a/Tests/NgrokitTests/DataHandleTests.swift b/Tests/NgrokitTests/DataHandleTests.swift new file mode 100644 index 0000000..fef2105 --- /dev/null +++ b/Tests/NgrokitTests/DataHandleTests.swift @@ -0,0 +1,40 @@ +// +// DataHandleTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +@testable import Ngrokit +import NgrokitMocks +import XCTest + +internal class DataHandleTests: XCTestCase { + internal func testParseNgrokErrorCode() throws { + let dataHandle = MockDataHandle.withNgrokCode() + let error = try dataHandle.parseNgrokErrorCode() + XCTAssertEqual(error.rawValue, 108) + } +} diff --git a/Tests/NgrokitTests/NgrokClientTests.swift b/Tests/NgrokitTests/NgrokClientTests.swift new file mode 100644 index 0000000..aea24fb --- /dev/null +++ b/Tests/NgrokitTests/NgrokClientTests.swift @@ -0,0 +1,130 @@ +// +// NgrokClientTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +@testable import Ngrokit +import NgrokitMocks +import NgrokOpenAPIClient +import XCTest + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +internal class NgrokClientTests: XCTestCase { + private func assertTunnelEqual( + _ actualOutput: Tunnel, + _ expectedOutput: Components.Schemas.TunnelResponse + ) { + XCTAssertEqual(actualOutput.publicURL.absoluteString, expectedOutput.public_url) + XCTAssertEqual(actualOutput.name, expectedOutput.name) + XCTAssertEqual(actualOutput.config.addr.absoluteString, expectedOutput.config.addr) + XCTAssertEqual(actualOutput.config.inspect, expectedOutput.config.inspect) + } + + // swiftlint:disable:next function_body_length + internal func testStartTunnel() async throws { + let publicURL = URL.temporaryDirectory() + let expectedInput = TunnelRequest( + port: .random(in: 10 ... 100), + name: UUID().uuidString, + proto: UUID().uuidString + ) + let expectedOutput: Components.Schemas.TunnelResponse = + .init( + name: UUID().uuidString, + public_url: publicURL.absoluteString, + config: .init(addr: UUID().uuidString, inspect: .random()) + ) + let request = Operations.startTunnel.Output.created( + .init(body: .json(expectedOutput)) + ) + let api = MockAPI(actualStartTunnelResult: .success(request)) + let client = NgrokClient(underlyingClient: api) + let actualOutput = try await client.startTunnel(expectedInput) + + assertTunnelEqual(actualOutput, expectedOutput) + + let body = await api.startTunnelPassed.last?.body + + guard case let .json(actualInput) = body else { + XCTFail("Incorrect result \(String(describing: body))") + return + } + + XCTAssertEqual(actualInput.name, expectedInput.name) + XCTAssertEqual(actualInput.addr, expectedInput.addr) + XCTAssertEqual(actualInput.proto, expectedInput.proto) + } + + internal func testStopTunnel() async throws { + let expectedInput = UUID().uuidString + let api = MockAPI(actualStopTunnelResult: .success(.noContent(.init()))) + let client = NgrokClient(underlyingClient: api) + + try await client.stopTunnel(withName: expectedInput) + + let name = await api.stopTunnelPassed.last?.path.name + + guard let actualInput = name else { + XCTFail("Incorrect name \(String(describing: name))") + return + } + + XCTAssertEqual(actualInput, expectedInput) + } + + internal func testListTunnel() async throws { + let expectedTunnels: [Components.Schemas.TunnelResponse] = [ + .init( + name: UUID().uuidString, + public_url: URL.temporaryDirectory().absoluteString, + config: .init(addr: UUID().uuidString, inspect: .random()) + ), + .init( + name: UUID().uuidString, + public_url: URL.temporaryDirectory().absoluteString, + config: .init(addr: UUID().uuidString, inspect: .random()) + ), + .init( + name: UUID().uuidString, + public_url: URL.temporaryDirectory().absoluteString, + config: .init(addr: UUID().uuidString, inspect: .random()) + ) + ] + let api = MockAPI( + actualListTunnelResult: .success( + .ok(.init(body: .json(.init(tunnels: expectedTunnels)))) + ) + ) + let client = NgrokClient(underlyingClient: api) + let actualOutput = try await client.listTunnels() + + zip(actualOutput, expectedTunnels).forEach(assertTunnelEqual) + } +} diff --git a/Tests/NgrokitTests/NgrokErrorTests.swift b/Tests/NgrokitTests/NgrokErrorTests.swift new file mode 100644 index 0000000..4673c88 --- /dev/null +++ b/Tests/NgrokitTests/NgrokErrorTests.swift @@ -0,0 +1,95 @@ +// +// NgrokErrorTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Ngrokit +import XCTest + +internal class NgrokErrorTests: XCTestCase { + // swiftlint:disable line_length + internal func testErrorDescriptions() { + XCTAssertEqual(NgrokError.invalidMetadataLength.errorDescription, "Invalid metadata length") + XCTAssertEqual(NgrokError.accountLimitExceeded.errorDescription, "You've hit your account limit for simultaneous ngrok agent sessions. Try stopping an existing agent or upgrading your account.") + XCTAssertEqual(NgrokError.unsupportedAgentVersion.errorDescription, "Your ngrok agent version is no longer supported. Only the most recent version of the ngrok agent is supported without an account. Update to a newer version with ngrok update or by downloading from https://ngrok.com/download. Sign up for an account to avoid forced version upgrades: https://ngrok.com/signup.") + XCTAssertEqual(NgrokError.captchaFailed.errorDescription, "You failed to solve the captcha, please try again.") + XCTAssertEqual(NgrokError.accountViolation.errorDescription, "You are disallowed from creating an ngrok account due to violation of the terms of service.") + XCTAssertEqual(NgrokError.gatewayError.errorDescription, "Ngrok gateway error. The server returned an invalid or incomplete HTTP response. Try starting ngrok with the full upstream service URL (e.g. ngrok http https://localhost:8081)") + XCTAssertEqual(NgrokError.tunnelNotFound.errorDescription, "Tunnel not found. This could be because your agent is not online or your tunnel has been flagged by our automated moderation system.") + XCTAssertEqual(NgrokError.accountBanned.errorDescription, "The account associated with this hostname has been banned. We've determined this account to be in violation of ngrok's terms of service. If you are the account owner and believe this is a mistake, please contact support@ngrok.com.") + XCTAssertEqual(NgrokError.passwordTooShort.errorDescription, "Your password must be at least 10 characters.") + XCTAssertEqual(NgrokError.accountCreationNotAllowed.errorDescription, "You may not create a new account because you are already a member of a free account. Upgrade or delete that account first before creating a new account.") + XCTAssertEqual(NgrokError.invalidCredentials.errorDescription, "The email or password you entered is not valid.") + XCTAssertEqual(NgrokError.userAlreadyExists.errorDescription, "A user with the email address already exists.") + XCTAssertEqual(NgrokError.disallowedEmailProvider.errorDescription, "Sign-ups are disallowed for the email provider. Please sign up with a different email provider.") + XCTAssertEqual(NgrokError.htmlContentSignupRequired.errorDescription, "Before you can serve HTML content, you must sign up for an ngrok account and install your authtoken.") + XCTAssertEqual(NgrokError.websiteVisitWarning.errorDescription, "You are about to visit HOSTPORT, served by SERVINGIP. This website is served for free through ngrok.com. You should only visit this website if you trust whoever sent the link to you.") + XCTAssertEqual(NgrokError.tunnelConnectionFailed.errorDescription, "Traffic was successfully tunneled to the ngrok agent, but the agent failed to establish a connection to the upstream web service") + } + + // swiftlint:disable nslocalizedstring_require_bundle + internal func testLocalizedDescriptions() { + XCTAssertEqual(NgrokError.invalidMetadataLength.localizedDescription, NSLocalizedString("Invalid metadata length", comment: "")) + XCTAssertEqual(NgrokError.accountLimitExceeded.localizedDescription, NSLocalizedString("You've hit your account limit for simultaneous ngrok agent sessions. Try stopping an existing agent or upgrading your account.", comment: "")) + XCTAssertEqual(NgrokError.unsupportedAgentVersion.localizedDescription, NSLocalizedString("Your ngrok agent version is no longer supported. Only the most recent version of the ngrok agent is supported without an account. Update to a newer version with ngrok update or by downloading from https://ngrok.com/download. Sign up for an account to avoid forced version upgrades: https://ngrok.com/signup.", comment: "")) + XCTAssertEqual(NgrokError.captchaFailed.localizedDescription, NSLocalizedString("You failed to solve the captcha, please try again.", comment: "")) + XCTAssertEqual(NgrokError.accountViolation.localizedDescription, NSLocalizedString("You are disallowed from creating an ngrok account due to violation of the terms of service.", comment: "")) + XCTAssertEqual(NgrokError.gatewayError.localizedDescription, NSLocalizedString("Ngrok gateway error. The server returned an invalid or incomplete HTTP response. Try starting ngrok with the full upstream service URL (e.g. ngrok http https://localhost:8081)", comment: "")) + XCTAssertEqual(NgrokError.tunnelNotFound.localizedDescription, NSLocalizedString("Tunnel not found. This could be because your agent is not online or your tunnel has been flagged by our automated moderation system.", comment: "")) + XCTAssertEqual(NgrokError.accountBanned.localizedDescription, NSLocalizedString("The account associated with this hostname has been banned. We've determined this account to be in violation of ngrok's terms of service. If you are the account owner and believe this is a mistake, please contact support@ngrok.com.", comment: "")) + XCTAssertEqual(NgrokError.passwordTooShort.localizedDescription, NSLocalizedString("Your password must be at least 10 characters.", comment: "")) + XCTAssertEqual(NgrokError.accountCreationNotAllowed.localizedDescription, NSLocalizedString("You may not create a new account because you are already a member of a free account. Upgrade or delete that account first before creating a new account.", comment: "")) + XCTAssertEqual(NgrokError.invalidCredentials.localizedDescription, NSLocalizedString("The email or password you entered is not valid.", comment: "")) + XCTAssertEqual(NgrokError.userAlreadyExists.localizedDescription, NSLocalizedString("A user with the email address already exists.", comment: "")) + XCTAssertEqual(NgrokError.disallowedEmailProvider.localizedDescription, NSLocalizedString("Sign-ups are disallowed for the email provider. Please sign up with a different email provider.", comment: "")) + XCTAssertEqual(NgrokError.htmlContentSignupRequired.localizedDescription, NSLocalizedString("Before you can serve HTML content, you must sign up for an ngrok account and install your authtoken.", comment: "")) + XCTAssertEqual(NgrokError.websiteVisitWarning.localizedDescription, NSLocalizedString("You are about to visit HOSTPORT, served by SERVINGIP. This website is served for free through ngrok.com. You should only visit this website if you trust whoever sent the link to you.", comment: "")) + XCTAssertEqual(NgrokError.tunnelConnectionFailed.localizedDescription, NSLocalizedString("Traffic was successfully tunneled to the ngrok agent, but the agent failed to establish a connection to the upstream web service", comment: "")) + } + + // swiftlint:enable nslocalizedstring_require_bundle + // swiftlint:enable line_length + + internal func testRawValues() { + XCTAssertEqual(NgrokError.invalidMetadataLength.rawValue, 100) + XCTAssertEqual(NgrokError.accountLimitExceeded.rawValue, 108) + XCTAssertEqual(NgrokError.unsupportedAgentVersion.rawValue, 120) + XCTAssertEqual(NgrokError.captchaFailed.rawValue, 1_205) + XCTAssertEqual(NgrokError.accountViolation.rawValue, 1_226) + XCTAssertEqual(NgrokError.gatewayError.rawValue, 3_004) + XCTAssertEqual(NgrokError.tunnelNotFound.rawValue, 3_200) + XCTAssertEqual(NgrokError.accountBanned.rawValue, 3_208) + XCTAssertEqual(NgrokError.passwordTooShort.rawValue, 4_011) + XCTAssertEqual(NgrokError.accountCreationNotAllowed.rawValue, 4_013) + XCTAssertEqual(NgrokError.invalidCredentials.rawValue, 4_100) + XCTAssertEqual(NgrokError.userAlreadyExists.rawValue, 4_101) + XCTAssertEqual(NgrokError.disallowedEmailProvider.rawValue, 4_108) + XCTAssertEqual(NgrokError.htmlContentSignupRequired.rawValue, 6_022) + XCTAssertEqual(NgrokError.websiteVisitWarning.rawValue, 6_024) + XCTAssertEqual(NgrokError.tunnelConnectionFailed.rawValue, 8_012) + } +} diff --git a/Tests/NgrokitTests/NgrokMacProcessTests.swift b/Tests/NgrokitTests/NgrokMacProcessTests.swift new file mode 100644 index 0000000..bf28497 --- /dev/null +++ b/Tests/NgrokitTests/NgrokMacProcessTests.swift @@ -0,0 +1,91 @@ +// +// NgrokMacProcessTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +// +// NgrokMacProcessTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +@testable import Ngrokit +import NgrokitMocks +import XCTest + +internal class NgrokMacProcessTests: XCTestCase { + internal func testInit() async { + let ngrokPath = UUID().uuidString + let httpPort = Int.random(in: 10 ... 10_000) + let process = NgrokMacProcess( + ngrokPath: ngrokPath, + httpPort: httpPort, + processType: MockProcess.self + ) + let actualProcess = await process.process + XCTAssertEqual(actualProcess.executableFilePath, ngrokPath) + XCTAssertEqual(actualProcess.port, httpPort) + XCTAssertEqual(actualProcess.scheme, "http") + } + + internal func testRunOnError() async throws { + let ngrokPath = UUID().uuidString + let httpPort = Int.random(in: 10 ... 10_000) + let process = NgrokMacProcess( + ngrokPath: ngrokPath, + httpPort: httpPort, + processType: MockProcess.self + ) + try await process.run { _ in } + + let actualProcess = await process.process + XCTAssertTrue(actualProcess.isRunCalled) + XCTAssertTrue(actualProcess.isTerminationHandlerSet) + } +} diff --git a/Tests/NgrokitTests/NgrokProcessCLIAPITests.swift b/Tests/NgrokitTests/NgrokProcessCLIAPITests.swift new file mode 100644 index 0000000..ba23309 --- /dev/null +++ b/Tests/NgrokitTests/NgrokProcessCLIAPITests.swift @@ -0,0 +1,53 @@ +// +// NgrokProcessCLIAPITests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +@testable import Ngrokit +import NgrokitMocks +import XCTest + +internal class NgrokProcessCLIAPITests: XCTestCase { + internal func testProcess() async throws { + let ngrokPath = UUID().uuidString + let httpPort = Int.random(in: 10 ... 10_000) + let api = NgrokProcessCLIAPI(ngrokPath: ngrokPath) + let process = api.process(forHTTPPort: httpPort) + + let macProcess = process as? NgrokMacProcess + + XCTAssertNotNil(macProcess) + + let mockProcess = await macProcess?.process + + XCTAssertNotNil(mockProcess) + + XCTAssertEqual(mockProcess?.executableFilePath, ngrokPath) + XCTAssertEqual(mockProcess?.port, httpPort) + XCTAssertEqual(mockProcess?.scheme, "http") + } +} diff --git a/Tests/SublimationTests/KVdbTests.swift b/Tests/SublimationTests/KVdbTests.swift new file mode 100644 index 0000000..5f3c387 --- /dev/null +++ b/Tests/SublimationTests/KVdbTests.swift @@ -0,0 +1,61 @@ +// +// KVdbTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Sublimation +import SublimationMocks +import XCTest + +// struct MockURL: KVdbURLConstructable { +// internal init(kvDBBase: String, keyBucketPath: String) { +// self.kvDBBase = kvDBBase +// self.keyBucketPath = keyBucketPath +// } +// +// let kvDBBase: String +// let keyBucketPath: String +// } + +internal class KVdbTests: XCTestCase { + internal func testPath() { + let key = UUID() + let bucket = UUID().uuidString + let actual = KVdb.path(forKey: key, atBucket: bucket) + XCTAssertEqual(actual, "/\(bucket)/\(key)") + } + + internal func testConstruct() { + let key = UUID() + let bucket = UUID().uuidString + let url = KVdb.construct(MockURL.self, forKey: key, atBucket: bucket) + let expectedPath = KVdb.path(forKey: key, atBucket: bucket) + + XCTAssertEqual(url.kvDBBase, KVdb.baseString) + XCTAssertEqual(url.keyBucketPath, expectedPath) + } +} diff --git a/Tests/SublimationTests/KVdbTunnelRepositoryFactoryTests.swift b/Tests/SublimationTests/KVdbTunnelRepositoryFactoryTests.swift new file mode 100644 index 0000000..0da99c1 --- /dev/null +++ b/Tests/SublimationTests/KVdbTunnelRepositoryFactoryTests.swift @@ -0,0 +1,69 @@ +// +// KVdbTunnelRepositoryFactoryTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Sublimation +import SublimationMocks +import XCTest + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +internal class KVdbTunnelRepositoryFactoryTests: XCTestCase { + internal func testSetupClient() async throws { + let getURLExpected: URL = .random() + let client = MockTunnelClient( + getValueResult: .success(getURLExpected), + saveValueError: nil + ) + let saveKey = UUID() + let saveURL: URL = .random() + + let getKey = UUID() + + let bucketName = UUID().uuidString + let factory = KVdbTunnelRepositoryFactory(bucketName: bucketName) + + let repository = factory.setupClient(client) + + try await repository.saveURL(saveURL, withKey: saveKey) + let getURLActual = try await repository.tunnel(forKey: getKey) + + let savedValue = await client.saveValuesPassed.last + XCTAssertEqual(saveKey, savedValue?.key) + XCTAssertEqual(saveURL, savedValue?.value) + XCTAssertEqual(bucketName, savedValue?.bucketName) + + let getValue = await client.getValuesPassed.last + XCTAssertEqual(getKey, getValue?.key) + XCTAssertEqual(bucketName, getValue?.bucketName) + + XCTAssertEqual(getURLActual, getURLExpected) + } +} diff --git a/Tests/SublimationTests/OptionalTests.swift b/Tests/SublimationTests/OptionalTests.swift new file mode 100644 index 0000000..a25dd2d --- /dev/null +++ b/Tests/SublimationTests/OptionalTests.swift @@ -0,0 +1,47 @@ +// +// OptionalTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +@testable import Sublimation +import XCTest + +internal class OptionalTests: XCTestCase { + internal func testFlatTuple() { + let nilValue: Int? = nil + let notNilValue: Int? = 12 + let expectedNotNil = (12, 12) + + XCTAssertNil(nilValue.flatTuple(notNilValue)) + XCTAssertNil(nilValue.flatTuple(nilValue)) + XCTAssertNil(notNilValue.flatTuple(nilValue)) + + let actualNotNil = notNilValue.flatTuple(notNilValue) + XCTAssertEqual(actualNotNil?.0, expectedNotNil.0) + XCTAssertEqual(actualNotNil?.1, expectedNotNil.1) + } +} diff --git a/Tests/SublimationTests/ResultTests.swift b/Tests/SublimationTests/ResultTests.swift new file mode 100644 index 0000000..803e457 --- /dev/null +++ b/Tests/SublimationTests/ResultTests.swift @@ -0,0 +1,49 @@ +// +// ResultTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +@testable import Sublimation +import SublimationMocks +import XCTest + +internal class ResultTests: XCTestCase { + internal typealias MockResult = Result + internal func testInit() { + let successValue = UUID() + let errorValue = UUID() + + let successResult = MockResult(success: successValue, failure: nil) + let failedResult = MockResult(success: UUID(), failure: MockError.value(errorValue)) + let emptyResult = MockResult(success: nil, failure: nil) + + let actualErrorValue: UUID? = failedResult.mockErrorValue() + try XCTAssertEqual(successResult.get(), successValue) + XCTAssertEqual(actualErrorValue, errorValue) + XCTAssert(emptyResult.error is Result.EmptyError) + } +} diff --git a/Tests/SublimationTests/URLTests.swift b/Tests/SublimationTests/URLTests.swift new file mode 100644 index 0000000..312f609 --- /dev/null +++ b/Tests/SublimationTests/URLTests.swift @@ -0,0 +1,39 @@ +// +// URLTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import XCTest + +internal class URLTests: XCTestCase { + internal func testKVdbURLConstructable() { + let base = "http://www.apple.com" + let keyBucketPath = UUID().uuidString + let url = URL(kvDBBase: base, keyBucketPath: keyBucketPath) + XCTAssertEqual(url.absoluteString, "\(base)/\(keyBucketPath)") + } +} diff --git a/Tests/SublimationVaporTests/MockServerApplication.swift b/Tests/SublimationVaporTests/MockServerApplication.swift new file mode 100644 index 0000000..6b530e7 --- /dev/null +++ b/Tests/SublimationVaporTests/MockServerApplication.swift @@ -0,0 +1,37 @@ +// +// MockServerApplication.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Logging +@testable import SublimationVapor +import XCTest + +internal struct MockServerApplication: ServerApplication { + internal let httpServerConfigurationPort: Int + internal let logger: Logger +} diff --git a/Tests/SublimationVaporTests/MockServerDelegate.swift b/Tests/SublimationVaporTests/MockServerDelegate.swift new file mode 100644 index 0000000..e3edc2f --- /dev/null +++ b/Tests/SublimationVaporTests/MockServerDelegate.swift @@ -0,0 +1,47 @@ +// +// MockServerDelegate.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Ngrokit +import NgrokitMocks +@testable import SublimationVapor +import XCTest + +internal final class MockServerDelegate: NgrokServerDelegate { + internal let id: UUID + + internal init(id: UUID) { + self.id = id + } + + internal func server( + _: any SublimationVapor.NgrokServer, updatedTunnel _: Ngrokit.Tunnel + ) {} + + internal func server(_: any SublimationVapor.NgrokServer, errorDidOccur _: any Error) {} +} diff --git a/Tests/SublimationVaporTests/NetworkResultTests.swift b/Tests/SublimationVaporTests/NetworkResultTests.swift new file mode 100644 index 0000000..7b4597f --- /dev/null +++ b/Tests/SublimationVaporTests/NetworkResultTests.swift @@ -0,0 +1,216 @@ +// +// NetworkResultTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import AsyncHTTPClient +import OpenAPIRuntime +@testable import SublimationVapor +import XCTest + +internal func XCTAsyncAssert( + _ expression: @escaping () async throws -> Bool, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) async rethrows { + let expressionResult = try await expression() + XCTAssert(expressionResult, message(), file: file, line: line) +} + +internal class NetworkResultTests: XCTestCase { + // swiftlint:disable:next function_body_length + internal func testError() { + #if canImport(Network) + let posixError = HTTPClient.NWPOSIXError(.ECONNREFUSED, reason: "") + let clientPosixError = ClientError( + operationID: "", + operationInput: (), + causeDescription: "", + underlyingError: posixError + ) + let actualPosixError: HTTPClient.NWPOSIXError? = + NetworkResult(error: clientPosixError).underlyingClientError() + XCTAssertEqual( + actualPosixError?.errorCode, + posixError.errorCode + ) + #endif + let timeoutError = HTTPClientError.connectTimeout + + let clientTimeoutError = ClientError( + operationID: "", + operationInput: (), + causeDescription: "", + underlyingError: timeoutError + ) + + let actualTimeoutError: HTTPClientError? = + NetworkResult(error: clientTimeoutError).underlyingClientError() + XCTAssertEqual( + actualTimeoutError, + timeoutError + ) + + #if canImport(Network) + XCTAssert(NetworkResult(error: posixError).isFailure) + #endif + XCTAssert(NetworkResult(error: timeoutError).isFailure) + } + + // swiftlint:disable:next function_body_length + internal func testClosure() async { + #if canImport(Network) + let posixError = HTTPClient.NWPOSIXError(.ECONNREFUSED, reason: "") + let clientPosixError = ClientError( + operationID: "", + operationInput: (), + causeDescription: "", + underlyingError: posixError + ) + #endif + let timeoutError = HTTPClientError.connectTimeout + + let clientTimeoutError = ClientError( + operationID: "", + operationInput: (), + causeDescription: "", + underlyingError: timeoutError + ) + + #if canImport(Network) + let actualPosixError: HTTPClient.NWPOSIXError? = + await NetworkResult { throw clientPosixError }.underlyingClientError() + XCTAssertEqual( + actualPosixError?.errorCode, + posixError.errorCode + ) + #endif + + let actualTimeoutError: HTTPClientError? = + await NetworkResult { throw clientTimeoutError }.underlyingClientError() + XCTAssertEqual( + actualTimeoutError, + timeoutError + ) + + #if canImport(Network) + + await XCTAsyncAssert { await NetworkResult { throw posixError }.isFailure } + #endif + await XCTAsyncAssert { await NetworkResult { throw timeoutError }.isFailure } + await XCTAsyncAssert { await NetworkResult { throw timeoutError }.isFailure } + + await XCTAsyncAssert { await NetworkResult {}.isSuccess } + } + + // swiftlint:disable:next function_body_length + internal func testGet() async { + #if canImport(Network) + let posixError = HTTPClient.NWPOSIXError(.ECONNREFUSED, reason: "") + #endif + let timeoutError = HTTPClientError.connectTimeout + + #if canImport(Network) + let clientPosixError = ClientError( + operationID: "", + operationInput: (), + causeDescription: "", + underlyingError: posixError + ) + #endif + let clientTimeoutError = ClientError( + operationID: "", + operationInput: (), + causeDescription: "", + underlyingError: timeoutError + ) + let clientOtherError = ClientError( + operationID: "", + operationInput: (), + causeDescription: "", + underlyingError: URLError(.unknown) + ) + + #if canImport(Network) + do { + let value: Void? = try await NetworkResult { throw clientPosixError }.get() + XCTAssertNil(value) + } catch { + XCTAssertNil(error) + } + #endif + + do { + let value: Void? = try await NetworkResult { throw clientTimeoutError }.get() + XCTAssertNil(value) + } catch { + XCTAssertNil(error) + } + + var error: (any Error)? + do { + _ = try await NetworkResult { throw clientOtherError }.get() + error = nil + } catch let caughtError as ClientError { + error = caughtError + } catch { + XCTAssertNil(error) + } + XCTAssertNotNil(error) + + do { + let value: ()? = try await NetworkResult {}.get() + XCTAssertNotNil(value) + } catch { + XCTAssertNil(error) + } + } +} + +extension NetworkResult { + internal var isSuccess: Bool { + guard case .success = self else { + return false + } + return true + } + + internal var isFailure: Bool { + guard case .failure = self else { + return false + } + return true + } + + internal func underlyingClientError() -> Failure? { + guard case let .connectionRefused(clientError) = self else { + return nil + } + return clientError.underlyingError as? Failure + } +} diff --git a/Tests/SublimationVaporTests/NgrokCLIAPIConfigurationTests.swift b/Tests/SublimationVaporTests/NgrokCLIAPIConfigurationTests.swift new file mode 100644 index 0000000..3790264 --- /dev/null +++ b/Tests/SublimationVaporTests/NgrokCLIAPIConfigurationTests.swift @@ -0,0 +1,45 @@ +// +// NgrokCLIAPIConfigurationTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Logging +@testable import SublimationVapor +import XCTest + +internal class NgrokCLIAPIConfigurationTests: XCTestCase { + internal func testInit() { + let loggerLabel = UUID().uuidString + let application = MockServerApplication( + httpServerConfigurationPort: .random(in: 10 ... 10_000), + logger: .init(label: loggerLabel) + ) + let configuration = NgrokCLIAPIConfiguration(serverApplication: application) + XCTAssertEqual(configuration.logger.label, loggerLabel) + XCTAssertEqual(configuration.port, application.httpServerConfigurationPort) + } +} diff --git a/Tests/SublimationVaporTests/NgrokCLIAPIServerFactoryTests.swift b/Tests/SublimationVaporTests/NgrokCLIAPIServerFactoryTests.swift new file mode 100644 index 0000000..fc9d525 --- /dev/null +++ b/Tests/SublimationVaporTests/NgrokCLIAPIServerFactoryTests.swift @@ -0,0 +1,68 @@ +// +// NgrokCLIAPIServerFactoryTests.swift +// Sublimation +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Ngrokit +import NgrokitMocks +@testable import SublimationVapor +import XCTest + +internal class NgrokCLIAPIServerFactoryTests: XCTestCase { + // swiftlint:disable:next function_body_length + internal func testServer() { + let loggerLabel = UUID().uuidString + let application = MockServerApplication( + httpServerConfigurationPort: .random(in: 10 ... 10_000), + logger: .init(label: loggerLabel) + ) + let delegateID = UUID() + let processID = UUID() + let configuration = NgrokCLIAPIConfiguration(serverApplication: application) + let factory = NgrokCLIAPIServerFactory( + cliAPI: MockNgrokCLIAPI(id: processID) + ) + let server = factory.server( + from: configuration, + handler: MockServerDelegate(id: delegateID) + ) + XCTAssertEqual( + (server.delegate as? MockServerDelegate)?.id, + delegateID + ) + + XCTAssertEqual( + server.port, + application.httpServerConfigurationPort + ) + + XCTAssertEqual( + (server.process as? MockNgrokProcess)?.id, + processID + ) + } +} diff --git a/generate.sh b/generate.sh new file mode 100755 index 0000000..c7c3d15 --- /dev/null +++ b/generate.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +swift run swift-openapi-generator generate \ + --output-directory Sources/NgrokOpenAPIClient \ + --config openapi-generator-config.yaml \ + openapi.yaml diff --git a/openapi-generator-config.yaml b/openapi-generator-config.yaml new file mode 100644 index 0000000..fb9b3e3 --- /dev/null +++ b/openapi-generator-config.yaml @@ -0,0 +1,4 @@ +generate: + - types + - client +accessModifier: package diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..020da0c --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,215 @@ +openapi: 3.0.0 +info: + title: Ngrok Agent API + version: 1.0.0 +servers: + - url: http://127.0.0.1:4040 + description: Default Local Server +paths: + /api: + get: + summary: Access the root API resource of a running ngrok agent + responses: + '200': + description: Successful response + /api/tunnels: + get: + summary: List Tunnels + operationId: listTunnels + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/TunnelList' + example: + tunnels: + - name: command_line + uri: /api/tunnels/command_line + public_url: https://d95211d2.ngrok.io + proto: https + config: + addr: localhost:80 + inspect: true + metrics: + conns: + count: 0 + gauge: 0 + rate1: 0 + rate5: 0 + rate15: 0 + p50: 0 + p90: 0 + p95: 0 + p99: 0 + http: + count: 0 + rate1: 0 + rate5: 0 + rate15: 0 + p50: 0 + p90: 0 + p95: 0 + p99: 0 + uri: /api/tunnels + post: + summary: Start tunnel + operationId: startTunnel + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TunnelRequest' + responses: + '201': + description: Tunnel started successfully + content: + application/json: + schema: + $ref: '#/components/schemas/TunnelResponse' + /api/tunnels/{name}: + get: + summary: Tunnel detail + operationId: getTunnel + parameters: + - name: name + in: path + required: true + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + example: + $ref: '#/components/schemas/TunnelResponse' + delete: + summary: Stop tunnel + operationId: stopTunnel + parameters: + - name: name + in: path + required: true + schema: + type: string + responses: + '204': + description: Tunnel stopped successfully + +components: + schemas: + TunnelList: + type: object + required: + - tunnels + properties: + tunnels: + type: array + items: + $ref: '#/components/schemas/TunnelResponse' + TunnelRequest: + type: object + properties: + addr: + type: string + proto: + type: string + name: + type: string + required: + - addr + - proto + - name + + TunnelResponse: + type: object + required: + - name + - public_url + - config + properties: + name: + type: string + uri: + type: string + format: uri + public_url: + type: string + format: uri + proto: + type: string + config: + type: object + properties: + addr: + type: string + inspect: + type: boolean + required: + - addr + - inspect + metrics: + type: object + properties: + conns: + type: object + properties: + count: + type: integer + gauge: + type: integer + rate1: + type: integer + rate5: + type: integer + rate15: + type: integer + p50: + type: integer + p90: + type: integer + p95: + type: integer + p99: + type: integer + required: + - count + - gauge + - rate1 + - rate5 + - rate15 + - p50 + - p90 + - p95 + - p99 + http: + type: object + properties: + count: + type: integer + rate1: + type: integer + rate5: + type: integer + rate15: + type: integer + p50: + type: integer + p90: + type: integer + p95: + type: integer + p99: + type: integer + required: + - count + - rate1 + - rate5 + - rate15 + - p50 + - p90 + - p95 + - p99 diff --git a/project.yml b/project.yml index 17e14ec..e202c98 100644 --- a/project.yml +++ b/project.yml @@ -4,6 +4,9 @@ settings: packages: Prch: path: . +projectReferences: + Demo: + path: ./Demo/SublimationDemoApp.xcodeproj aggregateTargets: Lint: buildScripts: diff --git a/scripts/generate.sh b/scripts/generate.sh new file mode 100755 index 0000000..b88e2dc --- /dev/null +++ b/scripts/generate.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +swift run swift-openapi-generator generate --output-directory Sources/Ngrokit/Generated --config openapi-generator-config.yaml openapi.yaml diff --git a/scripts/lint.sh b/scripts/lint.sh index cd8e125..fd0a0e3 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -26,11 +26,9 @@ if [ "$LINT_MODE" == "NONE" ]; then elif [ "$LINT_MODE" == "STRICT" ]; then SWIFTFORMAT_OPTIONS="" SWIFTLINT_OPTIONS="--strict" - STRINGSLINT_OPTIONS="--config .strict.stringslint.yml" else SWIFTFORMAT_OPTIONS="" SWIFTLINT_OPTIONS="" - STRINGSLINT_OPTIONS="--config .stringslint.yml" fi pushd $PACKAGE_DIR @@ -40,8 +38,6 @@ if [ -z "$CI" ]; then $MINT_RUN swiftlint autocorrect fi -$MINT_RUN periphery scan -$MINT_RUN stringslint lint $STRINGSLINT_OPTIONS $MINT_RUN swiftformat --lint $SWIFTFORMAT_OPTIONS . $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS