├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── actions │ ├── build-docs │ │ └── action.yml │ ├── ci │ │ └── action.yml │ ├── publish-docs │ │ └── action.yml │ ├── publish │ │ └── action.yml │ └── update-versions │ │ └── action.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── lint-pr-title.yml │ ├── manual-publish-docs.yml │ ├── manual-publish.yml │ ├── release-please.yml │ └── stale.yml ├── .gitignore ├── .jazzy.yaml ├── .release-please-manifest.json ├── .sdk_metadata.json ├── .sourcery.yml ├── .swift-version ├── .swiftlint.yml ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── ContractTests ├── .swiftlint.yml ├── Package.swift ├── Source │ ├── Controllers │ │ └── SdkController.swift │ ├── Models │ │ ├── client.swift │ │ ├── command.swift │ │ ├── hook.swift │ │ └── status.swift │ ├── main.swift │ └── routes.swift └── testharness-suppressions.txt ├── Framework ├── Info.plist └── module.modulemap ├── LICENSE.txt ├── LaunchDarkly.podspec ├── LaunchDarkly.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── LaunchDarkly_iOS.xcscheme │ ├── LaunchDarkly_macOS.xcscheme │ ├── LaunchDarkly_tvOS.xcscheme │ └── LaunchDarkly_watchOS.xcscheme ├── LaunchDarkly.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── xcschemes │ └── ContractTests.xcscheme ├── LaunchDarkly ├── GeneratedCode │ └── mocks.generated.swift ├── LaunchDarkly │ ├── Extensions │ │ ├── Data.swift │ │ ├── Date.swift │ │ ├── DateFormatter.swift │ │ └── Thread.swift │ ├── LDClient.swift │ ├── LDClientVariation.swift │ ├── LDCommon.swift │ ├── LDValueDecoder.swift │ ├── Models │ │ ├── ConnectionInformation.swift │ │ ├── Context │ │ │ ├── Kind.swift │ │ │ ├── LDContext.swift │ │ │ ├── Modifier.swift │ │ │ └── Reference.swift │ │ ├── DiagnosticEvent.swift │ │ ├── Event.swift │ │ ├── FeatureFlag │ │ │ ├── FeatureFlag.swift │ │ │ ├── FlagChange │ │ │ │ ├── ConnectionModeChangeObserver.swift │ │ │ │ ├── FlagChangeObserver.swift │ │ │ │ ├── FlagsUnchangedObserver.swift │ │ │ │ └── LDChangedFlag.swift │ │ │ ├── FlagRequestTracker.swift │ │ │ └── LDEvaluationDetail.swift │ │ ├── Hooks │ │ │ ├── EvaluationSeriesContext.swift │ │ │ ├── Hook.swift │ │ │ └── Metadata.swift │ │ ├── IdentifyTypes.swift │ │ └── LDConfig.swift │ ├── Networking │ │ ├── DarklyService.swift │ │ ├── HTTPHeaders.swift │ │ ├── HTTPURLRequest.swift │ │ ├── HTTPURLResponse.swift │ │ └── URLResponse.swift │ ├── ObjectiveC │ │ ├── ObjcLDApplicationInfo.swift │ │ ├── ObjcLDChangedFlag.swift │ │ ├── ObjcLDClient.swift │ │ ├── ObjcLDConfig.swift │ │ ├── ObjcLDContext.swift │ │ ├── ObjcLDEvaluationDetail.swift │ │ ├── ObjcLDReference.swift │ │ └── ObjcLDValue.swift │ ├── PrivacyInfo.xcprivacy │ ├── ServiceObjects │ │ ├── Cache │ │ │ ├── CacheConverter.swift │ │ │ ├── ConnectionInformationStore.swift │ │ │ ├── DiagnosticCache.swift │ │ │ ├── FeatureFlagCache.swift │ │ │ └── KeyedValueCache.swift │ │ ├── ClientServiceFactory.swift │ │ ├── CwlSysctl.swift │ │ ├── DiagnosticReporter.swift │ │ ├── EnvironmentReporter.swift │ │ ├── EnvironmentReporting │ │ │ ├── ApplicationInfoEnvironmentReporter.swift │ │ │ ├── EnvironmentReporterBuilder.swift │ │ │ ├── EnvironmentReporterChainBase.swift │ │ │ ├── IOSEnvironmentReporter.swift │ │ │ ├── MacOSEnvironmentReporter.swift │ │ │ ├── ReportingConsts.swift │ │ │ ├── SDKEnvironmentReporter.swift │ │ │ ├── SystemCapabilities.swift │ │ │ ├── TVOSEnvironmentReporter.swift │ │ │ └── WatchOSEnvironmentReporter.swift │ │ ├── EventReporter.swift │ │ ├── FlagChangeNotifier.swift │ │ ├── FlagStore.swift │ │ ├── FlagSynchronizer.swift │ │ ├── LDTimer.swift │ │ ├── Log.swift │ │ ├── NetworkReporter.swift │ │ ├── SheddingQueue.swift │ │ └── Throttler.swift │ ├── Support │ │ ├── Info.plist │ │ └── LaunchDarkly.h │ └── Util.swift └── LaunchDarklyTests │ ├── .swiftlint.yml │ ├── Extensions │ └── ThreadSpec.swift │ ├── Info.plist │ ├── LDClientHookSpec.swift │ ├── LDClientSpec.swift │ ├── LDValueDecoderSpec.swift │ ├── Mocks │ ├── ClientServiceMockFactory.swift │ ├── DarklyServiceMock.swift │ ├── EnvironmentReportingMock.swift │ ├── FlagMaintainingMock.swift │ ├── LDConfigStub.swift │ ├── LDContextStub.swift │ └── LDEventSourceMock.swift │ ├── Models │ ├── Context │ │ ├── KindSpec.swift │ │ ├── LDContextCodableSpec.swift │ │ ├── LDContextSpec.swift │ │ ├── ModifierSpec.swift │ │ └── ReferenceSpec.swift │ ├── DiagnosticEventSpec.swift │ ├── EventSpec.swift │ ├── FeatureFlag │ │ ├── FeatureFlagSpec.swift │ │ ├── FlagChange │ │ │ └── FlagChangeObserverSpec.swift │ │ └── FlagRequestTracking │ │ │ ├── FlagCounterSpec.swift │ │ │ └── FlagRequestTrackerSpec.swift │ └── LDConfigSpec.swift │ ├── Networking │ ├── DarklyServiceSpec.swift │ ├── HTTPHeadersSpec.swift │ ├── HTTPURLResponse.swift │ └── URLRequestSpec.swift │ ├── ServiceObjects │ ├── Cache │ │ ├── CacheConverterSpec.swift │ │ ├── DiagnosticCacheSpec.swift │ │ └── FeatureFlagCacheSpec.swift │ ├── DiagnosticReporterSpec.swift │ ├── EnvironmentReporting │ │ ├── ApplicationInfoEnvironmentReporterSpec.swift │ │ ├── EnvironmentReporterChainBaseSpec.swift │ │ ├── IOSEnvironmentReporterSpec.swift │ │ ├── SDKEnvironmentReporterSpec.swift │ │ └── WatchOSEnvironmentReporterSpec.swift │ ├── EventReporterSpec.swift │ ├── FlagChangeNotifierSpec.swift │ ├── FlagStoreSpec.swift │ ├── FlagSynchronizerSpec.swift │ ├── LDTimerSpec.swift │ ├── SheddingQueueSpec.swift │ ├── SynchronizingErrorSpec.swift │ └── ThrottlerSpec.swift │ ├── TestContext.swift │ ├── TestUtil.swift │ └── UtilSpec.swift ├── Makefile ├── Mintfile ├── Package.swift ├── README.md ├── SECURITY.md ├── SourceryTemplates └── mocks.stencil └── release-please-config.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Logs** 20 | If applicable, add any log output related to your problem. 21 | 22 | **Library version** 23 | The version that you are using. 24 | 25 | **XCode and Swift version** 26 | For instance, XCode 11.5, Swift 5.1. 27 | 28 | **Platform the issue occurs on** 29 | iPhone, iPad, macOS, tvOS, or watchOS. 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support request 4 | url: https://support.launchdarkly.com/hc/en-us/requests/new 5 | about: File your support requests with LaunchDarkly's support team 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I would love to see the library [...does something new...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/build-docs/action.yml: -------------------------------------------------------------------------------- 1 | name: Build Documentation 2 | description: 'Build Documentation.' 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Install jazzy gem 8 | shell: bash 9 | run: gem install jazzy 10 | 11 | - name: Build Documentation 12 | shell: bash 13 | run: jazzy -o docs 14 | -------------------------------------------------------------------------------- /.github/actions/ci/action.yml: -------------------------------------------------------------------------------- 1 | # This is a composite to allow sharing these steps into other workflows. 2 | # For instance it could be used by regular CI as well as the release process. 3 | 4 | name: CI Workflow 5 | description: 'Shared CI workflow.' 6 | inputs: 7 | xcode-version: 8 | description: 'Which version of xcode should be installed' 9 | required: true 10 | ios-sim: 11 | description: 'iOS Simulator to use for testing' 12 | required: true 13 | run-contract-tests: 14 | description: 'Should the contract tests be run?' 15 | required: true 16 | token: 17 | description: 'GH token used to download SDK test harness.' 18 | required: true 19 | 20 | runs: 21 | using: composite 22 | steps: 23 | - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd 24 | with: 25 | xcode-version: ${{ inputs.xcode-version }} 26 | 27 | - name: Install mint 28 | shell: bash 29 | run: | 30 | brew install mint 31 | 32 | - name: Install cocoapods 33 | shell: bash 34 | run: gem install cocoapods 35 | 36 | - name: Lint the podspec 37 | shell: bash 38 | run: pod lib lint LaunchDarkly.podspec --allow-warnings 39 | 40 | - name: Run swiftlint 41 | shell: bash 42 | run: | 43 | cd ./ContractTests 44 | swiftlint lint 45 | 46 | - name: Build for macOS 47 | shell: bash 48 | run: xcodebuild build -scheme 'LaunchDarkly_macOS' -sdk macosx -destination 'platform=macOS' | xcpretty 49 | 50 | - name: Build Tests for iOS device 51 | shell: bash 52 | run: xcodebuild build-for-testing -scheme 'LaunchDarkly_iOS' -sdk iphoneos CODE_SIGN_IDENTITY= | xcpretty 53 | 54 | - name: Build & Test on iOS Simulator 55 | shell: bash 56 | run: xcodebuild test -scheme 'LaunchDarkly_iOS' -sdk iphonesimulator -destination '${{ inputs.ios-sim }}' CODE_SIGN_IDENTITY= | xcpretty 57 | 58 | - name: Build for tvOS device 59 | shell: bash 60 | run: xcodebuild build -scheme 'LaunchDarkly_tvOS' -sdk appletvos CODE_SIGN_IDENTITY= | xcpretty 61 | 62 | - name: Build for tvOS Simulator 63 | shell: bash 64 | run: xcodebuild build -scheme 'LaunchDarkly_tvOS' -sdk appletvsimulator -destination 'platform=tvOS Simulator,name=Apple TV' | xcpretty 65 | 66 | - name: Build for watchOS simulator 67 | shell: bash 68 | run: xcodebuild build -scheme 'LaunchDarkly_watchOS' -sdk watchsimulator | xcpretty 69 | 70 | - name: Build for watchOS device 71 | shell: bash 72 | run: xcodebuild build -scheme 'LaunchDarkly_watchOS' -sdk watchos | xcpretty 73 | 74 | - name: Build & Test with swiftpm 75 | shell: bash 76 | run: swift test -v 77 | 78 | - name: Build contract tests 79 | if: ${{ inputs.run-contract-tests == 'true' }} 80 | shell: bash 81 | run: make build-contract-tests 82 | 83 | - name: Start contract tests in background 84 | if: ${{ inputs.run-contract-tests == 'true' }} 85 | shell: bash 86 | run: make start-contract-test-service-bg 87 | 88 | - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.0.2 89 | if: ${{ inputs.run-contract-tests == 'true' }} 90 | with: 91 | test_service_port: 8080 92 | token: ${{ inputs.token }} 93 | extra_params: "-status-timeout 120 -skip-from ./ContractTests/testharness-suppressions.txt" 94 | -------------------------------------------------------------------------------- /.github/actions/publish-docs/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | description: 'Publish the documentation to GitHub pages' 3 | inputs: 4 | token: 5 | description: 'Token to use for publishing.' 6 | required: true 7 | 8 | runs: 9 | using: composite 10 | steps: 11 | - uses: launchdarkly/gh-actions/actions/publish-pages@publish-pages-v1.0.2 12 | name: 'Publish to GitHub pages' 13 | with: 14 | docs_path: docs 15 | github_token: ${{ inputs.token }} 16 | -------------------------------------------------------------------------------- /.github/actions/publish/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | description: 'Publish the package to Cocoapods' 3 | inputs: 4 | dry_run: 5 | description: 'Is this a dry run. If so no package will be published.' 6 | required: true 7 | 8 | runs: 9 | using: composite 10 | steps: 11 | - name: Push to cocoapods 12 | if: ${{ inputs.dry_run == 'false' }} 13 | shell: bash 14 | run: pod trunk push LaunchDarkly.podspec --allow-warnings --verbose 15 | -------------------------------------------------------------------------------- /.github/actions/update-versions/action.yml: -------------------------------------------------------------------------------- 1 | name: Update xcode project version numbers 2 | description: 'Update xcode project version numbers' 3 | inputs: 4 | branch: 5 | description: 'The branch to checkout and push updates to' 6 | required: true 7 | 8 | runs: 9 | using: composite 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | ref: ${{ inputs.branch }} 14 | 15 | - name: Calculate version numbers 16 | id: version 17 | shell: bash 18 | run: | 19 | version=$(jq -r '."."' .release-please-manifest.json) 20 | major=$(echo "$version" | cut -f1 -d.) 21 | minor=$(echo "$version" | cut -f2 -d.) 22 | patch=$(echo "$version" | cut -f3 -d.) 23 | # 64 + version gives us a letter offset for the framework version. 24 | framework=$(echo $((major + 64)) | awk '{ printf("%c", $1) }') 25 | 26 | echo "major=${major}" >> "$GITHUB_OUTPUT" 27 | echo "minor=${minor}" >> "$GITHUB_OUTPUT" 28 | echo "patch=${patch}" >> "$GITHUB_OUTPUT" 29 | echo "framework=${framework}" >> "$GITHUB_OUTPUT" 30 | 31 | - name: Update other version numbers 32 | shell: bash 33 | run: | 34 | sed -i .bak -E \ 35 | -e 's/MARKETING_VERSION = [^;]+/MARKETING_VERSION = ${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}.${{ steps.version.outputs.patch }}/' \ 36 | -e 's/DYLIB_CURRENT_VERSION = [^;]+/DYLIB_CURRENT_VERSION = ${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}.${{ steps.version.outputs.patch }}/' \ 37 | -e 's/DYLIB_COMPATIBILITY_VERSION = [^;]+/DYLIB_COMPATIBILITY_VERSION = ${{ steps.version.outputs.major }}.0.0/' \ 38 | -e 's/FRAMEWORK_VERSION = .*/FRAMEWORK_VERSION = ${{ steps.version.outputs.framework }};/' \ 39 | LaunchDarkly.xcodeproj/project.pbxproj 40 | 41 | sed -i .bak -E \ 42 | -e "s/pod 'LaunchDarkly', '~> [0-9]+.[0-9]+'/pod 'LaunchDarkly', '~> ${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}'/" \ 43 | -e "s/github \"launchdarkly\/ios-client-sdk\" ~> [0-9]+.[0-9]+/github \"launchdarkly\/ios-client-sdk\" ~> ${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}/" README.md 44 | 45 | rm -f LaunchDarkly.xcodeproj/project.pbxproj.bak README.md.bak 46 | if [ $(git status --porcelain | wc -l) -gt 0 ]; then 47 | git config --global user.name 'LaunchDarklyReleaseBot' 48 | git config --global user.email 'LaunchDarklyReleaseBot@launchdarkly.com' 49 | 50 | git add LaunchDarkly.xcodeproj/project.pbxproj 51 | git add README.md 52 | 53 | git commit -m 'Updating generated project and readme files' 54 | git push 55 | fi 56 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Requirements** 2 | 3 | - [ ] I have added test coverage for new or changed functionality 4 | - [ ] I have followed the repository's [pull request submission guidelines](../blob/v9/CONTRIBUTING.md#submitting-pull-requests) 5 | - [ ] I have validated my changes against all supported platform versions 6 | 7 | **Related issues** 8 | 9 | Provide links to any issues in this repository or elsewhere relating to this pull request. 10 | 11 | **Describe the solution you've provided** 12 | 13 | Provide a clear and concise description of what you expect to happen. 14 | 15 | **Describe alternatives you've considered** 16 | 17 | Provide a clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | 21 | Add any other context about the pull request here. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run CI 2 | on: 3 | push: 4 | branches: [ v9, 'feat/**' ] 5 | paths-ignore: 6 | - '**.md' # Do not need to run CI for markdown changes. 7 | pull_request: 8 | branches: [ v9, 'feat/**' ] 9 | paths-ignore: 10 | - '**.md' 11 | 12 | jobs: 13 | macos-build: 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - xcode-version: 15.0.1 21 | ios-sim: 'platform=iOS Simulator,name=iPhone 15,OS=17.2' 22 | os: macos-13 23 | run-contract-tests: true 24 | - xcode-version: 14.3.1 25 | ios-sim: 'platform=iOS Simulator,name=iPhone 14,OS=16.4' 26 | os: macos-13 27 | run-contract-tests: false 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 # If you only need the current version keep this. 33 | 34 | - uses: ./.github/actions/ci 35 | with: 36 | xcode-version: ${{ matrix.xcode-version }} 37 | ios-sim: ${{ matrix.ios-sim }} 38 | run-contract-tests: ${{ matrix.run-contract-tests }} 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - uses: ./.github/actions/build-docs 42 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | lint-pr-title: 12 | uses: launchdarkly/gh-actions/.github/workflows/lint-pr-title.yml@main 13 | -------------------------------------------------------------------------------- /.github/workflows/manual-publish-docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: Publish Documentation 5 | jobs: 6 | build-publish: 7 | runs-on: macos-13 8 | 9 | permissions: 10 | id-token: write # Needed if using OIDC to get release secrets. 11 | contents: write # Needed in this case to write github pages. 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Build and Test 17 | uses: ./.github/actions/ci 18 | with: 19 | xcode-version: 15.0.1 20 | ios-sim: 'platform=iOS Simulator,name=iPhone 15,OS=17.2' 21 | run-contract-tests: true 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - uses: ./.github/actions/build-docs 25 | 26 | - uses: ./.github/actions/publish-docs 27 | with: 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/manual-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | dry_run: 6 | description: 'Is this a dry run. If so no package will be published.' 7 | type: boolean 8 | required: true 9 | 10 | jobs: 11 | build-publish: 12 | runs-on: macos-13 13 | 14 | # Needed to get tokens during publishing. 15 | permissions: 16 | id-token: write 17 | contents: read 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 23 | name: 'Get Cocoapods token' 24 | with: 25 | aws_assume_role: ${{ vars.AWS_ROLE_ARN }} 26 | ssm_parameter_pairs: '/production/common/releasing/cocoapods/token = COCOAPODS_TRUNK_TOKEN' 27 | 28 | - uses: ./.github/actions/ci 29 | with: 30 | xcode-version: 15.0.1 31 | ios-sim: 'platform=iOS Simulator,name=iPhone 15,OS=17.2' 32 | run-contract-tests: true 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - uses: ./.github/actions/publish 36 | with: 37 | dry_run: ${{ inputs.dry_run }} 38 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Run Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - v9 7 | 8 | jobs: 9 | release-package: 10 | runs-on: macos-13 11 | 12 | permissions: 13 | id-token: write # Needed if using OIDC to get release secrets. 14 | contents: write # Contents and pull-requests are for release-please to make releases. 15 | pull-requests: write 16 | 17 | steps: 18 | - uses: googleapis/release-please-action@v4 19 | id: release 20 | with: 21 | target-branch: ${{ github.ref_name }} 22 | 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 # If you only need the current version keep this. 26 | 27 | # 28 | # This step runs and updates an existing PR 29 | # 30 | - uses: ./.github/actions/update-versions 31 | if: ${{ steps.release.outputs.prs_created == 'true' }} 32 | with: 33 | branch: ${{ fromJSON(steps.release.outputs.pr).headBranchName }} 34 | 35 | # 36 | # These remaining steps are ONLY run if a release was actually created 37 | # 38 | - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 39 | if: ${{ steps.release.outputs.releases_created == 'true' }} 40 | name: 'Get Cocoapods token' 41 | with: 42 | aws_assume_role: ${{ vars.AWS_ROLE_ARN }} 43 | ssm_parameter_pairs: '/production/common/releasing/cocoapods/token = COCOAPODS_TRUNK_TOKEN' 44 | 45 | - uses: ./.github/actions/ci 46 | if: ${{ steps.release.outputs.releases_created == 'true' }} 47 | with: 48 | xcode-version: 15.0.1 49 | ios-sim: 'platform=iOS Simulator,name=iPhone 15,OS=17.2' 50 | run-contract-tests: true 51 | token: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - uses: ./.github/actions/build-docs 54 | if: ${{ steps.release.outputs.releases_created == 'true' }} 55 | 56 | - uses: ./.github/actions/publish 57 | if: ${{ steps.release.outputs.releases_created == 'true' }} 58 | with: 59 | token: ${{secrets.GITHUB_TOKEN}} 60 | dry_run: false 61 | 62 | - uses: ./.github/actions/publish-docs 63 | if: ${{ steps.release.outputs.releases_created == 'true' }} 64 | with: 65 | token: ${{secrets.GITHUB_TOKEN}} 66 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # Happen once per day at 1:30 AM 6 | - cron: '30 1 * * *' 7 | 8 | jobs: 9 | sdk-close-stale: 10 | uses: launchdarkly/gh-actions/.github/workflows/sdk-stale.yml@main 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xcuserstate 3 | xcuserdata 4 | *.playground 5 | /default.profraw 6 | /build 7 | **/.build 8 | /docs 9 | /Carthage/Checkouts 10 | **/.swiftpm 11 | **/Package.resolved 12 | /LaunchDarkly.xcworkspace/xcshareddata/swiftpm/Package.resolved 13 | /LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved 14 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | module: LaunchDarkly 2 | author: LaunchDarkly 3 | author_url: https://launchdarkly.com 4 | github_url: https://github.com/launchdarkly/ios-client-sdk 5 | clean: true 6 | swift_build_tool: spm 7 | readme: README.md 8 | documentation: 9 | - CHANGELOG.md 10 | - CONTRIBUTING.md 11 | - LICENSE.txt 12 | 13 | copyright: 'Copyright © 2019 Catamorphic Co.' 14 | 15 | custom_categories: 16 | - name: Core Classes 17 | children: 18 | - LDClient 19 | - LDConfig 20 | - LDContext 21 | - LDContextBuilder 22 | - Reference 23 | - LDMultiContextBuilder 24 | - LDEvaluationDetail 25 | - LDValue 26 | 27 | - name: Flag Change Observers 28 | children: 29 | - LDObserverOwner 30 | - LDChangedFlag 31 | - LDFlagChangeHandler 32 | - LDFlagCollectionChangeHandler 33 | - LDFlagsUnchangedHandler 34 | 35 | - name: Connection Information 36 | children: 37 | - ConnectionInformation 38 | - LDConnectionModeChangedHandler 39 | 40 | - name: Other Types 41 | children: 42 | - LDStreamingMode 43 | - LDFlagKey 44 | - LDInvalidArgumentError 45 | - RequestHeaderTransform 46 | 47 | - name: Objective-C Core Interfaces 48 | children: 49 | - ObjcLDClient 50 | - ObjcLDConfig 51 | - ObjcLDReference 52 | - ObjcLDContext 53 | - ObjcLDChangedFlag 54 | - ObjcLDValue 55 | - ObjcLDValueType 56 | 57 | - name: Objective-C EvaluationDetail Wrappers 58 | children: 59 | - ObjcLDBoolEvaluationDetail 60 | - ObjcLDIntegerEvaluationDetail 61 | - ObjcLDDoubleEvaluationDetail 62 | - ObjcLDStringEvaluationDetail 63 | - ObjcLDJSONEvaluationDetail 64 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "9.13.0" 3 | } 4 | -------------------------------------------------------------------------------- /.sdk_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "sdks": { 4 | "swift-client-sdk": { 5 | "name": "iOS SDK", 6 | "type": "client-side", 7 | "languages": [ 8 | "Swift", "Objective-C" 9 | ], 10 | "userAgents": ["iOS", "macOS", "tvOS", "watchOS"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.sourcery.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | - LaunchDarkly/ 3 | templates: 4 | - SourceryTemplates/ 5 | output: LaunchDarkly/GeneratedCode/ 6 | 7 | args: 8 | app: LaunchDarkly 9 | imports: 10 | - Foundation 11 | - LDSwiftEventSource 12 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # See test subconfiguration at `LaunchDarkly/LaunchDarklyTests/.swiftlint.yml` 2 | 3 | disabled_rules: 4 | - line_length 5 | 6 | opt_in_rules: 7 | - contains_over_filter_count 8 | - contains_over_filter_is_empty 9 | - contains_over_first_not_nil 10 | - contains_over_range_nil_comparison 11 | - empty_count 12 | - first_where 13 | - flatmap_over_map_reduce 14 | - implicitly_unwrapped_optional 15 | - let_var_whitespace 16 | - missing_docs 17 | - redundant_nil_coalescing 18 | - sorted_first_last 19 | - trailing_closure 20 | - unused_declaration 21 | - unused_import 22 | - vertical_whitespace_closing_braces 23 | 24 | included: 25 | - LaunchDarkly/LaunchDarkly 26 | - LaunchDarkly/LaunchDarklyTests 27 | 28 | excluded: 29 | 30 | function_body_length: 31 | warning: 50 32 | error: 70 33 | 34 | type_body_length: 35 | warning: 300 36 | error: 500 37 | 38 | file_length: 39 | warning: 1000 40 | error: 1500 41 | 42 | identifier_name: 43 | min_length: # only min_length 44 | warning: 3 # only warning 45 | max_length: 46 | warning: 50 47 | error: 60 48 | excluded: 49 | - id 50 | - URL 51 | - url 52 | - obj 53 | - key 54 | - all 55 | - tag 56 | - lhs 57 | - rhs 58 | 59 | trailing_whitespace: 60 | severity: error 61 | 62 | missing_docs: 63 | error: 64 | - open 65 | - public 66 | 67 | reporter: "xcode" 68 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Repository Maintainers 2 | * @launchdarkly/team-sdk-swift 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to the LaunchDarkly SDK for iOS 2 | ================================================ 3 | 4 | LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. 5 | 6 | Submitting bug reports and feature requests 7 | ------------------ 8 | 9 | The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/ios-client-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. 10 | 11 | Submitting pull requests 12 | ------------------ 13 | 14 | We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. 15 | 16 | Build instructions 17 | ------------------ 18 | 19 | ### Prerequisites 20 | 21 | This SDK is built with [Xcode](https://developer.apple.com/xcode/). This version is built and tested with Xcode 11.5. 22 | 23 | [Mint](https://github.com/yonaskolb/Mint) is used to manage dev tooling ([SwiftLint](https://github.com/realm/SwiftLint) and [Sourcery](https://github.com/krzysztofzablocki/Sourcery)). The build is set up so these are not required for building the current code in the repository, but Sourcery is used to regenerate test mocks so it may be required when building the test target after changes to the SDK code. Install `mint` with `brew install mint`. 24 | 25 | ### Building 26 | 27 | The exact command used to build the SDK depends on where you want to use it (for example -- iOS, watchOS, etc.). Refer to the `xcodebuild` commands in the SDK's [continuous integration build configuration](.github/workflows/ci.yml) for examples on how to build for the different platforms. 28 | 29 | If you wish to clean your working directory between builds, include the `clean` goal in your `xcodebuild` command(s). 30 | 31 | ### Testing 32 | 33 | To build the SDK and run all unit tests, include the `test` goal in your `xcodebuild` command(s). 34 | -------------------------------------------------------------------------------- /ContractTests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # See test subconfiguration at `LaunchDarkly/LaunchDarklyTests/.swiftlint.yml` 2 | 3 | disabled_rules: 4 | - cyclomatic_complexity 5 | - line_length 6 | - todo 7 | 8 | opt_in_rules: 9 | - contains_over_filter_count 10 | - contains_over_filter_is_empty 11 | - contains_over_first_not_nil 12 | - contains_over_range_nil_comparison 13 | - empty_count 14 | - first_where 15 | - flatmap_over_map_reduce 16 | - implicitly_unwrapped_optional 17 | - let_var_whitespace 18 | - missing_docs 19 | - redundant_nil_coalescing 20 | - sorted_first_last 21 | - trailing_closure 22 | - unused_declaration 23 | - unused_import 24 | - vertical_whitespace_closing_braces 25 | 26 | included: 27 | - Source 28 | 29 | excluded: 30 | 31 | function_body_length: 32 | warning: 70 33 | error: 90 34 | 35 | type_body_length: 36 | warning: 300 37 | error: 500 38 | 39 | file_length: 40 | warning: 1000 41 | error: 1500 42 | 43 | identifier_name: 44 | min_length: # only min_length 45 | warning: 2 # only warning 46 | max_length: 47 | warning: 50 48 | error: 60 49 | excluded: 50 | - id 51 | - URL 52 | - url 53 | - obj 54 | - key 55 | - all 56 | - tag 57 | - lhs 58 | - rhs 59 | 60 | trailing_whitespace: 61 | severity: error 62 | 63 | missing_docs: 64 | error: 65 | - open 66 | - public 67 | 68 | reporter: "xcode" 69 | -------------------------------------------------------------------------------- /ContractTests/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ContractTests", 7 | platforms: [ 8 | .iOS(.v12), 9 | .macOS(.v10_15), 10 | .watchOS(.v4), 11 | .tvOS(.v12) 12 | ], 13 | products: [ 14 | .executable( 15 | name: "ContractTests", 16 | targets: ["ContractTests"]) 17 | ], 18 | dependencies: [ 19 | Package.Dependency.package(name: "LaunchDarkly", path: ".."), 20 | .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0") 21 | ], 22 | targets: [ 23 | .target( 24 | name: "ContractTests", 25 | dependencies: [ 26 | .product(name: "LaunchDarkly", package: "LaunchDarkly"), 27 | .product(name: "Vapor", package: "vapor") 28 | ], 29 | path: "Source") 30 | ], 31 | swiftLanguageVersions: [.v5]) 32 | -------------------------------------------------------------------------------- /ContractTests/Source/Models/client.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import LaunchDarkly 3 | 4 | struct CreateInstance: Content { 5 | var tag: String? 6 | var configuration: Configuration 7 | } 8 | 9 | struct Configuration: Content { 10 | var credential: String 11 | var startWaitTimeMs: Double? 12 | var initCanFail: Bool? 13 | // TODO(mmk) Add serviceEndpoints 14 | var streaming: StreamingParameters? 15 | var polling: PollingParameters? 16 | var events: EventParameters? 17 | var tags: TagParameters? 18 | var clientSide: ClientSideParameters 19 | var hooks: HookParameters? 20 | } 21 | 22 | struct StreamingParameters: Content { 23 | var baseUri: String? 24 | var initialRetryDelayMs: Int? 25 | } 26 | 27 | struct PollingParameters: Content { 28 | var baseUri: String? 29 | // TODO(mmk) Add pollIntervalMs 30 | } 31 | 32 | struct EventParameters: Content { 33 | var baseUri: String? 34 | var capacity: Int? 35 | var enableDiagnostics: Bool? 36 | var allAttributesPrivate: Bool? 37 | var globalPrivateAttributes: [String]? 38 | var flushIntervalMs: Double? 39 | var enableGzip: Bool? 40 | } 41 | 42 | struct TagParameters: Content { 43 | var applicationId: String? 44 | var applicationName: String? 45 | var applicationVersion: String? 46 | var applicationVersionName: String? 47 | } 48 | 49 | struct ClientSideParameters: Content { 50 | var initialContext: LDContext 51 | var evaluationReasons: Bool? 52 | var useReport: Bool? 53 | var includeEnvironmentAttributes: Bool? 54 | } 55 | 56 | struct HookParameters: Content { 57 | var hooks: [HookParameter] 58 | } 59 | 60 | struct HookParameter: Content { 61 | var name: String 62 | var callbackUri: String 63 | var data: [String: [String: LDValue]]? 64 | var errors: [String: LDValue]? 65 | } 66 | -------------------------------------------------------------------------------- /ContractTests/Source/Models/command.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import LaunchDarkly 3 | 4 | enum CommandResponse: Content, Encodable { 5 | case evaluateFlag(EvaluateFlagResponse) 6 | case evaluateAll(EvaluateAllFlagsResponse) 7 | case contextBuild(ContextBuildResponse) 8 | case contextConvert(ContextBuildResponse) 9 | case contextComparison(ContextComparisonResponse) 10 | case ok 11 | 12 | func encode(to encoder: Encoder) throws { 13 | var container = encoder.singleValueContainer() 14 | 15 | switch self { 16 | case .evaluateFlag(let response): 17 | try container.encode(response) 18 | return 19 | case .evaluateAll(let response): 20 | try container.encode(response) 21 | return 22 | case .contextBuild(let response): 23 | try container.encode(response) 24 | return 25 | case .contextConvert(let response): 26 | try container.encode(response) 27 | return 28 | case .contextComparison(let response): 29 | try container.encode(response) 30 | return 31 | case .ok: 32 | try container.encode(true) 33 | return 34 | } 35 | } 36 | } 37 | 38 | struct CommandParameters: Content { 39 | var command: String 40 | var evaluate: EvaluateFlagParameters? 41 | var evaluateAll: EvaluateAllFlagsParameters? 42 | var customEvent: CustomEventParameters? 43 | var identifyEvent: IdentifyEventParameters? 44 | var contextBuild: ContextBuildParameters? 45 | var contextConvert: ContextConvertParameters? 46 | var contextComparison: ContextComparisonPairParameters? 47 | } 48 | 49 | struct EvaluateFlagParameters: Content { 50 | var flagKey: String 51 | var valueType: String 52 | var defaultValue: LDValue 53 | var detail: Bool 54 | } 55 | 56 | struct EvaluateFlagResponse: Content { 57 | var value: LDValue 58 | var variationIndex: Int? 59 | var reason: [String: LDValue]? 60 | } 61 | 62 | struct EvaluateAllFlagsParameters: Content { 63 | // TODO(mmk) Add support for withReasons, clientSideOnly, and detailsOnlyForTrackedFlags 64 | } 65 | 66 | struct EvaluateAllFlagsResponse: Content { 67 | var state: [LDFlagKey: LDValue]? 68 | } 69 | 70 | struct CustomEventParameters: Content { 71 | var eventKey: String 72 | var data: LDValue? 73 | var omitNullData: Bool 74 | var metricValue: Double? 75 | } 76 | 77 | struct IdentifyEventParameters: Content, Decodable { 78 | var context: LDContext 79 | } 80 | 81 | struct ContextBuildParameters: Content, Decodable { 82 | var single: SingleContextParameters? 83 | var multi: [SingleContextParameters]? 84 | } 85 | 86 | struct SingleContextParameters: Content, Decodable { 87 | var kind: String? 88 | var key: String 89 | var name: String? 90 | var anonymous: Bool? 91 | var privateAttribute: [String]? 92 | var custom: [String: LDValue]? 93 | 94 | private enum CodingKeys: String, CodingKey { 95 | case kind, key, name, anonymous, privateAttribute = "private", custom 96 | } 97 | } 98 | 99 | struct ContextBuildResponse: Content, Encodable { 100 | var output: String? 101 | var error: String? 102 | } 103 | 104 | struct ContextConvertParameters: Content, Decodable { 105 | var input: String 106 | } 107 | 108 | struct ContextComparisonPairParameters: Content, Decodable { 109 | var context1: ContextComparisonParameters 110 | var context2: ContextComparisonParameters 111 | } 112 | 113 | struct ContextComparisonParameters: Content, Decodable { 114 | var single: ContextComparisonSingleParams? 115 | var multi: [ContextComparisonSingleParams]? 116 | } 117 | 118 | struct ContextComparisonSingleParams: Content, Decodable { 119 | var kind: String 120 | var key: String 121 | var attributes: [AttributeDefinition]? 122 | var privateAttributes: [PrivateAttribute]? 123 | } 124 | 125 | struct AttributeDefinition: Content, Decodable { 126 | var name: String 127 | var value: LDValue 128 | } 129 | 130 | struct PrivateAttribute: Content, Decodable { 131 | var value: String 132 | var literal: Bool 133 | } 134 | 135 | struct ContextComparisonResponse: Content, Encodable { 136 | var equals: Bool 137 | } 138 | -------------------------------------------------------------------------------- /ContractTests/Source/Models/hook.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import LaunchDarkly 3 | 4 | class TestHook: Hook { 5 | private let name: String 6 | private let callbackUrl: URL 7 | private let data: [String: [String: Encodable]] 8 | private let errors: [String: LDValue] 9 | 10 | init(name: String, callbackUrl: URL, data: [String: [String: Encodable]], errors: [String: LDValue]) { 11 | self.name = name 12 | self.callbackUrl = callbackUrl 13 | self.data = data 14 | self.errors = errors 15 | } 16 | 17 | func metadata() -> Metadata { 18 | return Metadata(name: self.name) 19 | } 20 | 21 | func beforeEvaluation(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData) -> LaunchDarkly.EvaluationSeriesData { 22 | return processHook(seriesContext: seriesContext, seriesData: seriesData, evaluationDetail: nil, stage: "beforeEvaluation") 23 | } 24 | 25 | func afterEvaluation(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData, evaluationDetail: LDEvaluationDetail) -> EvaluationSeriesData { 26 | return processHook(seriesContext: seriesContext, seriesData: seriesData, evaluationDetail: evaluationDetail, stage: "afterEvaluation") 27 | } 28 | 29 | private func processHook(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData, evaluationDetail: LDEvaluationDetail?, stage: String) -> EvaluationSeriesData { 30 | guard self.errors[stage] == nil else { return seriesData } 31 | 32 | let payload = EvaluationPayload(evaluationSeriesContext: seriesContext, evaluationSeriesData: seriesData, stage: stage, evaluationDetail: evaluationDetail) 33 | 34 | // swiftlint:disable:next force_try 35 | let data = try! JSONEncoder().encode(payload) 36 | 37 | var request = URLRequest(url: self.callbackUrl) 38 | request.httpMethod = "POST" 39 | request.httpBody = data 40 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 41 | 42 | URLSession.shared.dataTask(with: request) { (_, _, _) in 43 | }.resume() 44 | 45 | var updatedData = seriesData 46 | if let be = self.data[stage] { 47 | be.forEach { (key, value) in 48 | updatedData[key] = value 49 | } 50 | } 51 | 52 | return updatedData 53 | } 54 | } 55 | 56 | struct EvaluationPayload: Encodable { 57 | var evaluationSeriesContext: EvaluationSeriesContext 58 | var evaluationSeriesData: EvaluationSeriesData 59 | var stage: String 60 | var evaluationDetail: LDEvaluationDetail? 61 | 62 | init(evaluationSeriesContext: EvaluationSeriesContext, evaluationSeriesData: EvaluationSeriesData, stage: String, evaluationDetail: LDEvaluationDetail? = nil) { 63 | self.evaluationSeriesContext = evaluationSeriesContext 64 | self.evaluationSeriesData = evaluationSeriesData 65 | self.stage = stage 66 | self.evaluationDetail = evaluationDetail 67 | } 68 | 69 | private enum CodingKeys: String, CodingKey { 70 | case evaluationSeriesContext 71 | case evaluationSeriesData 72 | case stage 73 | case evaluationDetail 74 | } 75 | 76 | struct DynamicKey: CodingKey { 77 | let intValue: Int? = nil 78 | let stringValue: String 79 | 80 | init?(intValue: Int) { 81 | return nil 82 | } 83 | 84 | init?(stringValue: String) { 85 | self.stringValue = stringValue 86 | } 87 | } 88 | 89 | public func encode(to encoder: any Encoder) throws { 90 | var container = encoder.container(keyedBy: CodingKeys.self) 91 | 92 | try container.encode(evaluationSeriesContext, forKey: .evaluationSeriesContext) 93 | try container.encode(stage, forKey: .stage) 94 | 95 | try container.encodeIfPresent(evaluationDetail, forKey: .evaluationDetail) 96 | 97 | var nested = container.nestedContainer(keyedBy: DynamicKey.self, forKey: .evaluationSeriesData) 98 | try evaluationSeriesData.forEach { (_, _) in 99 | try evaluationSeriesData.forEach { try nested.encode($1, forKey: DynamicKey(stringValue: $0)!) } 100 | } 101 | } 102 | } 103 | 104 | extension EvaluationSeriesContext: Encodable { 105 | private enum CodingKeys: String, CodingKey { 106 | case flagKey 107 | case context 108 | case defaultValue 109 | case method 110 | } 111 | 112 | public func encode(to encoder: any Encoder) throws { 113 | var container = encoder.container(keyedBy: CodingKeys.self) 114 | try container.encode(flagKey, forKey: .flagKey) 115 | try container.encode(context, forKey: .context) 116 | try container.encode(defaultValue, forKey: .defaultValue) 117 | try container.encode(methodName, forKey: .method) 118 | } 119 | } 120 | 121 | extension LDEvaluationDetail: Encodable where T == LDValue { 122 | private enum CodingKeys: String, CodingKey { 123 | case value 124 | case variationIndex 125 | case reason 126 | } 127 | 128 | public func encode(to encoder: any Encoder) throws { 129 | var container = encoder.container(keyedBy: CodingKeys.self) 130 | try container.encode(value, forKey: .value) 131 | try container.encode(variationIndex, forKey: .variationIndex) 132 | try container.encode(reason, forKey: .reason) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /ContractTests/Source/Models/status.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | struct StatusResponse: Content { 4 | var name: String 5 | var capabilities: [String] 6 | } 7 | -------------------------------------------------------------------------------- /ContractTests/Source/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Vapor 3 | 4 | let semaphore = DispatchSemaphore(value: 0) 5 | 6 | DispatchQueue.global(qos: .userInitiated).async { 7 | do { 8 | var env = try Environment.detect() 9 | try LoggingSystem.bootstrap(from: &env) 10 | let app = Application(env) 11 | defer { app.shutdown() } 12 | try routes(app) 13 | try app.run() 14 | } catch { 15 | } 16 | semaphore.signal() 17 | } 18 | 19 | let runLoop = RunLoop.current 20 | 21 | while semaphore.wait(timeout: .now()) == .timedOut { 22 | runLoop.run(mode: .default, before: .distantFuture) 23 | } 24 | -------------------------------------------------------------------------------- /ContractTests/Source/routes.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | func routes(_ app: Application) throws { 4 | let sdkController = SdkController() 5 | try app.register(collection: sdkController) 6 | } 7 | -------------------------------------------------------------------------------- /ContractTests/testharness-suppressions.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/ios-client-sdk/082430ff008d76d503327c52b27e3b2af99c3ef2/ContractTests/testharness-suppressions.txt -------------------------------------------------------------------------------- /Framework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Framework/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module LaunchDarkly { 2 | umbrella header "LaunchDarkly.h" 3 | export * 4 | module * { export * } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 Catamorphic, Co. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /LaunchDarkly.podspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | Pod::Spec.new do |ld| 3 | 4 | ld.name = "LaunchDarkly" 5 | ld.version = "9.13.0" # x-release-please-version 6 | ld.summary = "iOS SDK for LaunchDarkly" 7 | 8 | ld.description = <<-DESC 9 | LaunchDarkly is the feature management platform that software teams use to build better software, faster. Development teams use feature management as a best practice to separate code deployments from feature releases. With LaunchDarkly teams control their entire feature lifecycles from concept to launch to value. 10 | With LaunchDarkly, you can: 11 | * Release a new feature to a subset of your users, like a group of users who opt-in to a beta tester group. 12 | * Slowly roll out a feature to an increasing percentage of users and track the effect that feature has on key metrics. 13 | * Instantly turn off a feature that is causing problems, without re-deploying code or restarting the application with a changed config file. 14 | * Maintain granular control over your users’ experience by granting access to certain features based on any attribute you choose. For example, provide different users with different functionality based on their payment plan. 15 | * Disable parts of your application to facilitate maintenance, without taking everything offline. 16 | DESC 17 | 18 | ld.homepage = "https://github.com/launchdarkly/ios-client-sdk" 19 | 20 | ld.license = { :type => "Apache License, Version 2.0", :file => "LICENSE.txt" } 21 | 22 | ld.author = { "LaunchDarkly" => "sdks@launchdarkly.com" } 23 | 24 | ld.ios.deployment_target = "12.0" 25 | ld.watchos.deployment_target = "4.0" 26 | ld.tvos.deployment_target = "12.0" 27 | ld.osx.deployment_target = "10.13" 28 | 29 | ld.source = { :git => ld.homepage + '.git', :tag => ld.version} 30 | 31 | ld.source_files = "LaunchDarkly/LaunchDarkly/**/*.{h,m,swift}" 32 | ld.resource_bundles = { 33 | "#{ld.module_name}_Privacy" => 'LaunchDarkly/LaunchDarkly/PrivacyInfo.xcprivacy' 34 | } 35 | 36 | ld.requires_arc = true 37 | 38 | ld.swift_version = '5.0' 39 | 40 | ld.subspec 'Core' do |es| 41 | es.dependency 'LDSwiftEventSource', '3.3.0' 42 | es.dependency 'DataCompression', '3.8.0' 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /LaunchDarkly.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LaunchDarkly.xcodeproj/xcshareddata/xcschemes/LaunchDarkly_iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | 57 | 63 | 64 | 65 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | 76 | 86 | 87 | 93 | 94 | 95 | 96 | 102 | 103 | 109 | 110 | 111 | 112 | 114 | 115 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /LaunchDarkly.xcodeproj/xcshareddata/xcschemes/LaunchDarkly_macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /LaunchDarkly.xcodeproj/xcshareddata/xcschemes/LaunchDarkly_tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /LaunchDarkly.xcodeproj/xcshareddata/xcschemes/LaunchDarkly_watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /LaunchDarkly.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LaunchDarkly.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LaunchDarkly.xcworkspace/xcshareddata/xcschemes/ContractTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 11 | 14 | 15 | 16 | 17 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Extensions/Data.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | var base64UrlEncodedString: String { 5 | base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_") 6 | } 7 | 8 | var jsonDictionary: [String: Any]? { 9 | try? JSONSerialization.jsonObject(with: self, options: [.allowFragments]) as? [String: Any] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Extensions/Date.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Date { 4 | var millisSince1970: Int64 { 5 | Int64(floor(self.timeIntervalSince1970 * 1_000)) 6 | } 7 | 8 | init?(millisSince1970: Int64?) { 9 | guard let millisSince1970 = millisSince1970, millisSince1970 >= 0 10 | else { return nil } 11 | self = Date(timeIntervalSince1970: Double(millisSince1970) / 1_000) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Extensions/DateFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension DateFormatter { 4 | static var httpUrlHeaderFormatter: DateFormatter { 5 | let httpUrlHeaderFormatter = DateFormatter() 6 | httpUrlHeaderFormatter.locale = Locale(identifier: "en_US_POSIX") 7 | httpUrlHeaderFormatter.timeZone = TimeZone(abbreviation: "GMT") 8 | httpUrlHeaderFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" // Mon, 07 May 2018 19:46:29 GMT 9 | 10 | return httpUrlHeaderFormatter 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Extensions/Thread.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Thread { 4 | static func performOnMain(_ executionClosure: () -> Void) { 5 | guard Thread.isMainThread 6 | else { 7 | DispatchQueue.main.sync { 8 | executionClosure() 9 | } 10 | return 11 | } 12 | executionClosure() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/Context/Kind.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Kind is an enumeration set by the application to describe what kind of entity an `LDContext` 4 | /// represents. The meaning of this is completely up to the application. When no Kind is 5 | /// specified, the default is `Kind.user`. 6 | /// 7 | /// For a multi-context (see `LDMultiContextBuilder`), the Kind is always `Kind.multi`; 8 | /// there is a specific Kind for each of the individual Contexts within it. 9 | public enum Kind: Codable, Equatable, Hashable { 10 | /// user is both the default Kind and also the kind used for legacy users in earlier versions of this SDK. 11 | case user 12 | 13 | /// multi is only usable by constructing a multi-context using `LDMultiContextBuilder`. Attempting to set 14 | /// a context kind to multi directly will result in an invalid context. 15 | case multi 16 | 17 | /// The custom case handles arbitrarily defined contexts (e.g. org, account, server). 18 | case custom(String) 19 | 20 | public init(from decoder: Decoder) throws { 21 | let container = try decoder.singleValueContainer() 22 | 23 | switch try container.decode(String.self) { 24 | case "user": 25 | self = .user 26 | case "multi": 27 | self = .multi 28 | case let custom: 29 | self = .custom(custom) 30 | } 31 | } 32 | 33 | public func encode(to encoder: Encoder) throws { 34 | var container = encoder.singleValueContainer() 35 | try container.encode(self.description) 36 | } 37 | 38 | internal func isMulti() -> Bool { 39 | self == .multi || self == .custom("multi") 40 | } 41 | 42 | internal func isUser() -> Bool { 43 | self == .user || self == .custom("user") || self == .custom("") 44 | } 45 | 46 | private static func isValid(_ description: String) -> Bool { 47 | description.onlyContainsCharset(Util.validKindCharacterSet) 48 | } 49 | } 50 | 51 | extension Kind: Comparable { 52 | public static func < (lhs: Kind, rhs: Kind) -> Bool { 53 | lhs.description < rhs.description 54 | } 55 | 56 | public static func == (lhs: Kind, rhs: Kind) -> Bool { 57 | lhs.description == rhs.description 58 | } 59 | } 60 | 61 | extension Kind: LosslessStringConvertible { 62 | public init?(_ description: String) { 63 | switch description { 64 | case "kind": 65 | return nil 66 | case "multi": 67 | self = .multi 68 | case "", "user": 69 | self = .user 70 | default: 71 | if !Kind.isValid(description) { 72 | return nil 73 | } 74 | 75 | self = .custom(description) 76 | } 77 | } 78 | } 79 | 80 | extension Kind: CustomStringConvertible { 81 | public var description: String { 82 | switch self { 83 | case .user: 84 | return "user" 85 | case .multi: 86 | return "multi" 87 | case let .custom(val): 88 | return val 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum DiagnosticKind: String, Codable { 4 | case diagnosticInit = "diagnostic-init", 5 | diagnosticStats = "diagnostic" 6 | } 7 | 8 | protocol DiagnosticEvent { 9 | var kind: DiagnosticKind { get } 10 | var creationDate: Int64 { get } 11 | var id: DiagnosticId { get } 12 | } 13 | 14 | struct DiagnosticInit: DiagnosticEvent, Encodable { 15 | let kind = DiagnosticKind.diagnosticInit 16 | let id: DiagnosticId 17 | let creationDate: Int64 18 | 19 | let sdk: DiagnosticSdk 20 | let configuration: DiagnosticConfig 21 | let platform: DiagnosticPlatform 22 | 23 | init(config: LDConfig, environmentReporting: EnvironmentReporting, diagnosticId: DiagnosticId, creationDate: Int64) { 24 | self.id = diagnosticId 25 | self.creationDate = creationDate 26 | 27 | self.sdk = DiagnosticSdk(config: config) 28 | self.configuration = DiagnosticConfig(config: config) 29 | self.platform = DiagnosticPlatform(environmentReporting: environmentReporting) 30 | } 31 | } 32 | 33 | struct DiagnosticStats: DiagnosticEvent, Encodable { 34 | let kind = DiagnosticKind.diagnosticStats 35 | let id: DiagnosticId 36 | let creationDate: Int64 37 | 38 | let dataSinceDate: Int64 39 | let droppedEvents: Int 40 | let eventsInLastBatch: Int 41 | let streamInits: [DiagnosticStreamInit] 42 | } 43 | 44 | struct DiagnosticStreamInit: Codable { 45 | let timestamp: Int64 46 | let durationMillis: Int 47 | let failed: Bool 48 | } 49 | 50 | struct DiagnosticId: Codable { 51 | let diagnosticId: String 52 | let sdkKeySuffix: String 53 | 54 | init(diagnosticId: String, sdkKey: String) { 55 | self.diagnosticId = diagnosticId 56 | let suffixStart = sdkKey.index(sdkKey.startIndex, offsetBy: max(0, sdkKey.count - 6)) 57 | self.sdkKeySuffix = String(sdkKey[suffixStart...]) 58 | } 59 | } 60 | 61 | struct DiagnosticPlatform: Encodable { 62 | let name: String = "swift" 63 | let systemName: String 64 | let systemVersion: String 65 | let backgroundEnabled: Bool 66 | let streamingEnabled: Bool 67 | 68 | // Very general device model such as "iPad", "iPhone Simulator", or "Apple Watch" 69 | let deviceType: String 70 | 71 | init(environmentReporting: EnvironmentReporting) { 72 | systemName = SystemCapabilities.operatingSystem.rawValue 73 | systemVersion = environmentReporting.systemVersion 74 | backgroundEnabled = SystemCapabilities.operatingSystem.isBackgroundEnabled 75 | streamingEnabled = SystemCapabilities.operatingSystem.isStreamingEnabled 76 | deviceType = environmentReporting.deviceModel 77 | } 78 | } 79 | 80 | struct DiagnosticSdk: Encodable { 81 | let name: String = "ios-client-sdk" 82 | let version: String 83 | let wrapperName: String? 84 | let wrapperVersion: String? 85 | 86 | init(config: LDConfig) { 87 | version = ReportingConsts.sdkVersion 88 | wrapperName = config.wrapperName 89 | wrapperVersion = config.wrapperVersion 90 | } 91 | } 92 | 93 | struct DiagnosticConfig: Codable { 94 | let customBaseURI: Bool 95 | let customEventsURI: Bool 96 | let customStreamURI: Bool 97 | let eventsCapacity: Int 98 | let connectTimeoutMillis: Int 99 | let eventsFlushIntervalMillis: Int 100 | let streamingDisabled: Bool 101 | let allAttributesPrivate: Bool 102 | let pollingIntervalMillis: Int 103 | let backgroundPollingIntervalMillis: Int 104 | let useReport: Bool 105 | let backgroundPollingDisabled: Bool 106 | let evaluationReasonsRequested: Bool 107 | let maxCachedContexts: Int 108 | let mobileKeyCount: Int 109 | let diagnosticRecordingIntervalMillis: Int 110 | let customHeaders: Bool 111 | 112 | init(config: LDConfig) { 113 | customBaseURI = config.baseUrl != LDConfig.Defaults.baseUrl 114 | customEventsURI = config.eventsUrl != LDConfig.Defaults.eventsUrl 115 | customStreamURI = config.streamUrl != LDConfig.Defaults.streamUrl 116 | eventsCapacity = config.eventCapacity 117 | connectTimeoutMillis = Int(exactly: round(config.connectionTimeout * 1_000)) ?? .max 118 | eventsFlushIntervalMillis = Int(exactly: round(config.eventFlushInterval * 1_000)) ?? .max 119 | streamingDisabled = config.streamingMode == .polling 120 | allAttributesPrivate = config.allContextAttributesPrivate 121 | pollingIntervalMillis = Int(exactly: round(config.flagPollingInterval * 1_000)) ?? .max 122 | backgroundPollingIntervalMillis = Int(exactly: round(config.backgroundFlagPollingInterval * 1_000)) ?? .max 123 | useReport = config.useReport 124 | backgroundPollingDisabled = !config.enableBackgroundUpdates 125 | evaluationReasonsRequested = config.evaluationReasons 126 | // While the SDK treats all negative values as unlimited, for consistency we only send -1 for diagnostics 127 | maxCachedContexts = config.maxCachedContexts >= 0 ? config.maxCachedContexts : -1 128 | mobileKeyCount = 1 + (config.getSecondaryMobileKeys().count) 129 | diagnosticRecordingIntervalMillis = Int(exactly: round(config.diagnosticRecordingInterval * 1_000)) ?? .max 130 | customHeaders = !config.additionalHeaders.isEmpty || config.headerDelegate != nil 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/Event.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private protocol SubEvent { 4 | func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws 5 | } 6 | 7 | class Event: Encodable { 8 | enum CodingKeys: String, CodingKey { 9 | case key, previousKey, kind, creationDate, context, contextKeys, value, defaultValue = "default", variation, version, 10 | data, startDate, endDate, features, reason, metricValue, contextKind, previousContextKind 11 | } 12 | 13 | enum Kind: String { 14 | case feature, debug, identify, custom, summary 15 | 16 | static var allKinds: [Kind] { 17 | [feature, debug, identify, custom, summary] 18 | } 19 | } 20 | 21 | let kind: Kind 22 | 23 | fileprivate init(kind: Kind) { 24 | self.kind = kind 25 | } 26 | 27 | func encode(to encoder: Encoder) throws { 28 | var container = encoder.container(keyedBy: CodingKeys.self) 29 | try container.encode(kind.rawValue, forKey: .kind) 30 | switch self.kind { 31 | case .custom: try (self as? CustomEvent)?.encode(to: encoder, container: container) 32 | case .debug, .feature: try (self as? FeatureEvent)?.encode(to: encoder, container: container) 33 | case .identify: try (self as? IdentifyEvent)?.encode(to: encoder, container: container) 34 | case .summary: try (self as? SummaryEvent)?.encode(to: encoder, container: container) 35 | } 36 | } 37 | } 38 | 39 | class CustomEvent: Event, SubEvent { 40 | let key: String 41 | let context: LDContext 42 | let data: LDValue 43 | let metricValue: Double? 44 | let creationDate: Date 45 | 46 | init(key: String, context: LDContext, data: LDValue = nil, metricValue: Double? = nil, creationDate: Date = Date()) { 47 | self.key = key 48 | self.context = context 49 | self.data = data 50 | self.metricValue = metricValue 51 | self.creationDate = creationDate 52 | super.init(kind: Event.Kind.custom) 53 | } 54 | 55 | fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { 56 | var container = container 57 | try container.encode(key, forKey: .key) 58 | try container.encode(context, forKey: .context) 59 | 60 | if data != .null { 61 | try container.encode(data, forKey: .data) 62 | } 63 | try container.encodeIfPresent(metricValue, forKey: .metricValue) 64 | try container.encode(creationDate, forKey: .creationDate) 65 | } 66 | } 67 | 68 | class FeatureEvent: Event, SubEvent { 69 | let key: String 70 | let context: LDContext 71 | let value: LDValue 72 | let defaultValue: LDValue 73 | let featureFlag: FeatureFlag? 74 | let includeReason: Bool 75 | let creationDate: Date 76 | 77 | init(key: String, context: LDContext, value: LDValue, defaultValue: LDValue, featureFlag: FeatureFlag?, includeReason: Bool, isDebug: Bool, creationDate: Date = Date()) { 78 | self.key = key 79 | self.value = value 80 | self.defaultValue = defaultValue 81 | self.featureFlag = featureFlag 82 | self.includeReason = includeReason 83 | self.creationDate = creationDate 84 | 85 | if isDebug { 86 | self.context = context 87 | super.init(kind: .debug) 88 | } else { 89 | var newContext = LDContext(copyFrom: context) 90 | newContext.redactAnonymousAttributes = true 91 | self.context = newContext 92 | super.init(kind: .feature) 93 | } 94 | } 95 | 96 | fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { 97 | var container = container 98 | try container.encode(key, forKey: .key) 99 | try container.encode(context, forKey: .context) 100 | try container.encodeIfPresent(featureFlag?.variation, forKey: .variation) 101 | try container.encodeIfPresent(featureFlag?.versionForEvents, forKey: .version) 102 | try container.encode(value, forKey: .value) 103 | try container.encode(defaultValue, forKey: .defaultValue) 104 | if let reason = includeReason || featureFlag?.trackReason ?? false ? featureFlag?.reason : nil { 105 | try container.encode(reason, forKey: .reason) 106 | } 107 | try container.encode(creationDate, forKey: .creationDate) 108 | } 109 | } 110 | 111 | class IdentifyEvent: Event, SubEvent { 112 | let context: LDContext 113 | let creationDate: Date 114 | 115 | init(context: LDContext, creationDate: Date = Date()) { 116 | self.context = context 117 | self.creationDate = creationDate 118 | super.init(kind: .identify) 119 | } 120 | 121 | fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { 122 | var container = container 123 | try container.encode(context.fullyQualifiedKey(), forKey: .key) 124 | try container.encode(context, forKey: .context) 125 | try container.encode(creationDate, forKey: .creationDate) 126 | } 127 | } 128 | 129 | class SummaryEvent: Event, SubEvent { 130 | let flagRequestTracker: FlagRequestTracker 131 | let endDate: Date 132 | 133 | init(flagRequestTracker: FlagRequestTracker, endDate: Date = Date()) { 134 | self.flagRequestTracker = flagRequestTracker 135 | self.endDate = endDate 136 | super.init(kind: .summary) 137 | } 138 | 139 | fileprivate func encode(to encoder: Encoder, container: KeyedEncodingContainer) throws { 140 | var container = container 141 | try container.encode(flagRequestTracker.startDate, forKey: .startDate) 142 | try container.encode(endDate, forKey: .endDate) 143 | try container.encode(flagRequestTracker.flagCounters, forKey: .features) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/ConnectionModeChangeObserver.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ConnectionModeChangedObserver { 4 | private(set) weak var owner: LDObserverOwner? 5 | let connectionModeChangedHandler: LDConnectionModeChangedHandler 6 | 7 | init(owner: LDObserverOwner, connectionModeChangedHandler: @escaping LDConnectionModeChangedHandler) { 8 | self.owner = owner 9 | self.connectionModeChangedHandler = connectionModeChangedHandler 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagChangeObserver.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct FlagChangeObserver { 4 | private(set) weak var owner: LDObserverOwner? 5 | let flagKeys: [LDFlagKey] 6 | let flagChangeHandler: LDFlagChangeHandler? 7 | let flagCollectionChangeHandler: LDFlagCollectionChangeHandler? 8 | 9 | init(key: LDFlagKey, owner: LDObserverOwner, flagChangeHandler: @escaping LDFlagChangeHandler) { 10 | self.flagKeys = [key] 11 | self.owner = owner 12 | self.flagChangeHandler = flagChangeHandler 13 | self.flagCollectionChangeHandler = nil 14 | } 15 | 16 | init(keys: [LDFlagKey], owner: LDObserverOwner, flagCollectionChangeHandler: @escaping LDFlagCollectionChangeHandler) { 17 | self.flagKeys = keys 18 | self.owner = owner 19 | self.flagChangeHandler = nil 20 | self.flagCollectionChangeHandler = flagCollectionChangeHandler 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagsUnchangedObserver.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct FlagsUnchangedObserver { 4 | private(set) weak var owner: LDObserverOwner? 5 | let flagsUnchangedHandler: LDFlagsUnchangedHandler 6 | 7 | init(owner: LDObserverOwner, flagsUnchangedHandler: @escaping LDFlagsUnchangedHandler) { 8 | self.owner = owner 9 | self.flagsUnchangedHandler = flagsUnchangedHandler 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/LDChangedFlag.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Collects the elements of a feature flag that changed as a result of the SDK receiving an update. 5 | 6 | The SDK will pass a LDChangedFlag or a collection of LDChangedFlags into feature flag observer closures. See 7 | `LDClient.observe(key:owner:handler:)`, `LDClient.observe(keys:owner:handler:)`, and 8 | `LDClient.observeAll(owner:handler:)` for more details. 9 | */ 10 | public struct LDChangedFlag { 11 | /// The key of the changed feature flag 12 | public let key: LDFlagKey 13 | /// The feature flag's value before the change. 14 | public let oldValue: LDValue 15 | /// The feature flag's value after the change. 16 | public let newValue: LDValue 17 | 18 | init(key: LDFlagKey, oldValue: LDValue, newValue: LDValue) { 19 | self.key = key 20 | self.oldValue = oldValue 21 | self.newValue = newValue 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagRequestTracker.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | 4 | struct FlagRequestTracker { 5 | let startDate = Date() 6 | var flagCounters: [LDFlagKey: FlagCounter] = [:] 7 | let logger: OSLog 8 | 9 | init(logger: OSLog) { 10 | self.logger = logger 11 | } 12 | 13 | mutating func trackRequest(flagKey: LDFlagKey, reportedValue: LDValue, featureFlag: FeatureFlag?, defaultValue: LDValue, context: LDContext) { 14 | if flagCounters[flagKey] == nil { 15 | flagCounters[flagKey] = FlagCounter(defaultValue: defaultValue) 16 | } 17 | guard let flagCounter = flagCounters[flagKey] 18 | else { return } 19 | flagCounter.trackRequest(reportedValue: reportedValue, featureFlag: featureFlag, context: context) 20 | 21 | os_log("%s \n\tflagKey: %s\n\tvariation: %s\n\tversion: %s", log: logger, type: .debug, 22 | typeName(and: #function), 23 | flagKey, 24 | String(describing: featureFlag?.variation), 25 | String(describing: featureFlag?.flagVersion ?? featureFlag?.version)) 26 | } 27 | 28 | var hasLoggedRequests: Bool { !flagCounters.isEmpty } 29 | } 30 | 31 | extension FlagRequestTracker: TypeIdentifying { } 32 | 33 | final class FlagCounter: Encodable { 34 | enum CodingKeys: String, CodingKey { 35 | case defaultValue = "default", counters, contextKinds 36 | } 37 | 38 | enum CounterCodingKeys: String, CodingKey { 39 | case value, variation, version, unknown, count 40 | } 41 | 42 | private(set) var defaultValue: LDValue 43 | private(set) var flagValueCounters: [CounterKey: CounterValue] = [:] 44 | private(set) var contextKinds: Set = Set() 45 | 46 | init(defaultValue: LDValue) { 47 | // default value follows a "first one wins" approach where the first evaluation for a flag key sets the default value for the summary events 48 | self.defaultValue = defaultValue 49 | } 50 | 51 | func trackRequest(reportedValue: LDValue, featureFlag: FeatureFlag?, context: LDContext) { 52 | let key = CounterKey(variation: featureFlag?.variation, version: featureFlag?.versionForEvents) 53 | if let counter = flagValueCounters[key] { 54 | counter.increment() 55 | } else { 56 | flagValueCounters[key] = CounterValue(value: reportedValue) 57 | } 58 | 59 | context.contextKeys().forEach { kind, _ in 60 | contextKinds.insert(kind) 61 | } 62 | } 63 | 64 | func encode(to encoder: Encoder) throws { 65 | var container = encoder.container(keyedBy: CodingKeys.self) 66 | if defaultValue != .null { 67 | try container.encode(defaultValue, forKey: .defaultValue) 68 | } 69 | try container.encode(contextKinds, forKey: .contextKinds) 70 | var countersContainer = container.nestedUnkeyedContainer(forKey: .counters) 71 | try flagValueCounters.forEach { (key, value) in 72 | var counterContainer = countersContainer.nestedContainer(keyedBy: CounterCodingKeys.self) 73 | try counterContainer.encodeIfPresent(key.version, forKey: .version) 74 | try counterContainer.encodeIfPresent(key.variation, forKey: .variation) 75 | try counterContainer.encode(value.count, forKey: .count) 76 | try counterContainer.encode(value.value, forKey: .value) 77 | if key.version == nil { 78 | try counterContainer.encode(true, forKey: .unknown) 79 | } 80 | } 81 | } 82 | } 83 | 84 | struct CounterKey: Equatable, Hashable { 85 | let variation: Int? 86 | let version: Int? 87 | } 88 | 89 | class CounterValue { 90 | let value: LDValue 91 | private(set) var count: Int = 1 92 | 93 | init(value: LDValue) { 94 | self.value = value 95 | } 96 | 97 | func increment() { 98 | self.count += 1 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/FeatureFlag/LDEvaluationDetail.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | An object returned by the SDK's `variationDetail` methods, combining the result of a flag evaluation with an 5 | explanation of how it is calculated. 6 | */ 7 | public final class LDEvaluationDetail { 8 | /// The value of the flag for the current context. 9 | public let value: T 10 | /// The index of the returned value within the flag's list of variations, or `nil` if the default was returned. 11 | public let variationIndex: Int? 12 | /// A structure representing the main factor that influenced the resultant flag evaluation value. 13 | public let reason: [String: LDValue]? 14 | 15 | internal init(value: T, variationIndex: Int?, reason: [String: LDValue]?) { 16 | self.value = value 17 | self.variationIndex = variationIndex 18 | self.reason = reason 19 | } 20 | 21 | /// Apply the `transform` function to the detail's inner value property, converting an 22 | /// `LDEvaluationDetail` to an `LDEvaluationDetail`. 23 | public func map(transform: ((_: T) -> U)) -> LDEvaluationDetail { 24 | return LDEvaluationDetail( 25 | value: transform(self.value), 26 | variationIndex: self.variationIndex, 27 | reason: self.reason) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/Hooks/EvaluationSeriesContext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Contextual information that will be provided to handlers during evaluation series. 4 | public class EvaluationSeriesContext { 5 | /// The key of the flag being evaluated. 6 | public let flagKey: String 7 | /// The context in effect at the time of evaluation. 8 | public let context: LDContext 9 | /// The default value provided to the calling evaluation method. 10 | public let defaultValue: LDValue 11 | /// A string identifing the name of the method called. 12 | public let methodName: String 13 | 14 | init(flagKey: String, context: LDContext, defaultValue: LDValue, methodName: String) { 15 | self.flagKey = flagKey 16 | self.context = context 17 | self.defaultValue = defaultValue 18 | self.methodName = methodName 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/Hooks/Hook.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Implementation specific hook data for evaluation stages. 4 | /// 5 | /// Hook implementations can use this to store data needed between stages. 6 | public typealias EvaluationSeriesData = [String: Encodable] 7 | 8 | /// Protocol for extending SDK functionality via hooks. 9 | public protocol Hook { 10 | /// Get metadata about the hook implementation. 11 | func metadata() -> Metadata 12 | /// The before method is called during the execution of a variation method before the flag value has been 13 | /// determined. The method is executed synchronously. 14 | func beforeEvaluation(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData) -> EvaluationSeriesData 15 | /// The after method is called during the execution of the variation method after the flag value has been 16 | /// determined. The method is executed synchronously. 17 | func afterEvaluation(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData, evaluationDetail: LDEvaluationDetail) -> EvaluationSeriesData 18 | } 19 | 20 | public extension Hook { 21 | /// Get metadata about the hook implementation. 22 | func metadata() -> Metadata { 23 | return Metadata(name: "UNDEFINED") 24 | } 25 | 26 | /// The before method is called during the execution of a variation method before the flag value has been 27 | /// determined. The method is executed synchronously. 28 | func beforeEvaluation(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData) -> EvaluationSeriesData { 29 | return seriesData 30 | } 31 | 32 | /// The after method is called during the execution of the variation method after the flag value has been 33 | /// determined. The method is executed synchronously. 34 | func afterEvaluation(seriesContext: EvaluationSeriesContext, seriesData: EvaluationSeriesData, evaluationDetail: LDEvaluationDetail) -> EvaluationSeriesData { 35 | return seriesData 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/Hooks/Metadata.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Metadata data class used for annotating hook implementations. 4 | public class Metadata { 5 | private let name: String 6 | 7 | /// Initialize a new Metadata instance with the provided name. 8 | public init(name: String) { 9 | self.name = name 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Models/IdentifyTypes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Denotes the result of an identify request made through the `LDClient.identify(context: completion:)` method. 5 | */ 6 | public enum IdentifyResult { 7 | /** 8 | The identify request has completed successfully. 9 | */ 10 | case complete 11 | /** 12 | The identify request has received an unrecoverable failure. 13 | */ 14 | case error 15 | /** 16 | The identify request has been replaced with a subsequent request. Read `LDClient.identify(context: completion:)` for more details. 17 | */ 18 | case shed 19 | /** 20 | The identify request exceeded some time out parameter. Read `LDClient.identify(context: timeout: completion)` for more details. 21 | */ 22 | case timeout 23 | 24 | init(from: TaskResult) { 25 | switch from { 26 | case .complete: 27 | self = .complete 28 | case .error: 29 | self = .error 30 | case .shed: 31 | self = .shed 32 | } 33 | } 34 | } 35 | 36 | /** 37 | When a new `LDContext` is being identified, the SDK has a few choices it can make on how to handle intermediate flag evaluations 38 | until fresh values have been retrieved from the LaunchDarkly APIs. 39 | */ 40 | public enum IdentifyCacheUsage { 41 | /** 42 | `no` will not load any flag values from the cache. Instead it will maintain the current in memory state from the previously identified context. 43 | 44 | This method ensures the greatest continuity of experience until the identify network communication resolves. 45 | */ 46 | case no 47 | 48 | /** 49 | `yes` will clear the in memory state of any previously known flag values. The SDK will attempt to load cached flag values for the newly identified 50 | context. If no cache is found, the state remains empty until the network request resolves. 51 | */ 52 | case yes 53 | 54 | /** 55 | `ifAvailable` will attempt to load cached flag values for the newly identified context. If cached values are found, the in memory state is fully 56 | replaced with those values. 57 | 58 | If no cached values are found, the existing in memory state is retained until the network request resolves. 59 | */ 60 | case ifAvailable 61 | } 62 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct HTTPHeaders { 4 | 5 | struct HeaderKey { 6 | static let authorization = "Authorization" 7 | static let userAgent = "User-Agent" 8 | static let contentType = "Content-Type" 9 | static let accept = "Accept" 10 | static let eventSchema = "X-LaunchDarkly-Event-Schema" 11 | static let ifNoneMatch = "If-None-Match" 12 | static let eventPayloadIDHeader = "X-LaunchDarkly-Payload-ID" 13 | static let sdkWrapper = "X-LaunchDarkly-Wrapper" 14 | static let tags = "X-LaunchDarkly-Tags" 15 | } 16 | 17 | struct HeaderValue { 18 | static let apiKey = "api_key" 19 | static let applicationJson = "application/json" 20 | static let eventSchema4 = "4" 21 | } 22 | 23 | private let mobileKey: String 24 | private let additionalHeaders: [String: String] 25 | private let authKey: String 26 | private let userAgent: String 27 | private let wrapperHeaderVal: String? 28 | private let applicationTag: String 29 | 30 | init(config: LDConfig, environmentReporter: EnvironmentReporting) { 31 | self.mobileKey = config.mobileKey 32 | self.additionalHeaders = config.additionalHeaders 33 | self.userAgent = "\(SystemCapabilities.systemName)/\(ReportingConsts.sdkVersion)" 34 | self.authKey = "\(HeaderValue.apiKey) \(config.mobileKey)" 35 | self.applicationTag = environmentReporter.applicationInfo.buildTag() 36 | 37 | if let wrapperName = config.wrapperName { 38 | if let wrapperVersion = config.wrapperVersion { 39 | wrapperHeaderVal = "\(wrapperName)/\(wrapperVersion)" 40 | } else { 41 | wrapperHeaderVal = wrapperName 42 | } 43 | } else { 44 | wrapperHeaderVal = nil 45 | } 46 | } 47 | 48 | private var baseHeaders: [String: String] { 49 | var headers = [HeaderKey.authorization: authKey, 50 | HeaderKey.userAgent: userAgent] 51 | 52 | if let wrapperHeader = wrapperHeaderVal { 53 | headers[HeaderKey.sdkWrapper] = wrapperHeader 54 | } 55 | 56 | if !self.applicationTag.isEmpty { 57 | headers[HeaderKey.tags] = self.applicationTag 58 | } 59 | 60 | return headers 61 | } 62 | 63 | var eventSourceHeaders: [String: String] { withAdditionalHeaders(baseHeaders) } 64 | var flagRequestHeaders: [String: String] { withAdditionalHeaders(baseHeaders) } 65 | 66 | var eventRequestHeaders: [String: String] { 67 | var headers = baseHeaders 68 | headers[HeaderKey.contentType] = HeaderValue.applicationJson 69 | headers[HeaderKey.accept] = HeaderValue.applicationJson 70 | headers[HeaderKey.eventSchema] = HeaderValue.eventSchema4 71 | return withAdditionalHeaders(headers) 72 | } 73 | 74 | var diagnosticRequestHeaders: [String: String] { 75 | var headers = baseHeaders 76 | headers[HeaderKey.contentType] = HeaderValue.applicationJson 77 | headers[HeaderKey.accept] = HeaderValue.applicationJson 78 | return withAdditionalHeaders(headers) 79 | } 80 | 81 | private func withAdditionalHeaders(_ headers: [String: String]) -> [String: String] { 82 | headers.merging(additionalHeaders) { $1 } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Networking/HTTPURLRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URLRequest { 4 | struct HTTPMethods { 5 | static let get = "GET" 6 | static let post = "POST" 7 | static let report = "REPORT" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Networking/HTTPURLResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension HTTPURLResponse { 4 | 5 | struct HeaderKeys { 6 | static let date = "Date" 7 | static let etag = "Etag" 8 | } 9 | 10 | struct StatusCodes { 11 | // swiftlint:disable:next identifier_name 12 | static let ok = 200 13 | static let accepted = 202 14 | static let notModified = 304 15 | static let badRequest = 400 16 | static let unauthorized = 401 17 | static let methodNotAllowed = 405 18 | static let internalServerError = 500 19 | static let notImplemented = 501 20 | } 21 | 22 | var headerDate: Date? { 23 | guard let dateHeader = self.allHeaderFields[HeaderKeys.date] as? String 24 | else { return nil } 25 | return DateFormatter.httpUrlHeaderFormatter.date(from: dateHeader) 26 | } 27 | 28 | var headerEtag: String? { 29 | self.allHeaderFields[HeaderKeys.etag] as? String 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Networking/URLResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URLResponse { 4 | var httpStatusCode: Int? { (self as? HTTPURLResponse)?.statusCode } 5 | var httpHeaderEtag: String? { (self as? HTTPURLResponse)?.headerEtag } 6 | } 7 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDApplicationInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Use LDApplicationInfo to define application metadata. 5 | 6 | These properties are optional and informational. They may be used in LaunchDarkly analytics or other product features. 7 | */ 8 | @objc(LDApplicationInfo) 9 | public final class ObjcLDApplicationInfo: NSObject { 10 | internal var applicationInfo: ApplicationInfo 11 | 12 | @objc override public init() { 13 | applicationInfo = ApplicationInfo() 14 | } 15 | 16 | internal init(_ applicationInfo: ApplicationInfo?) { 17 | if let appInfo = applicationInfo { 18 | self.applicationInfo = appInfo 19 | } else { 20 | self.applicationInfo = ApplicationInfo() 21 | } 22 | } 23 | 24 | /// A unique identifier representing the application where the LaunchDarkly SDK is running. 25 | /// 26 | /// This can be specified as any string value as long as it only uses the following characters: 27 | /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other 28 | /// characters will be ignored. 29 | @objc public func applicationIdentifier(_ applicationId: String) { 30 | applicationInfo.applicationIdentifier(applicationId) 31 | } 32 | 33 | /// A human-friendly application name representing the application where the LaunchDarkly SDK is running. 34 | /// 35 | /// This can be specified as any string value as long as it only uses the following characters: 36 | /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other 37 | /// characters will be ignored. 38 | @objc public func applicationName(_ applicationName: String) { 39 | applicationInfo.applicationName(applicationName) 40 | } 41 | 42 | /// A unique identifier representing the version of the application where the LaunchDarkly SDK 43 | /// is running. 44 | /// 45 | /// This can be specified as any string value as long as it only uses the following characters: 46 | /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other 47 | /// characters will be ignored. 48 | @objc public func applicationVersion(_ applicationVersion: String) { 49 | applicationInfo.applicationVersion(applicationVersion) 50 | } 51 | 52 | /// A human-friendly name representing the version of the application where the LaunchDarkly SDK 53 | /// is running. 54 | /// 55 | /// This can be specified as any string value as long as it only uses the following characters: 56 | /// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other 57 | /// characters will be ignored. 58 | @objc public func applicationVersionName(_ applicationVersionName: String) { 59 | applicationInfo.applicationVersionName(applicationVersionName) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDChangedFlag.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Collects the elements of a feature flag that changed as a result of a `clientstream` update or feature flag request. The SDK will pass a typed ObjcLDChangedFlag or a collection of ObjcLDChangedFlags into feature flag observer blocks. This is the base type for the typed ObjcLDChangedFlags passed into observer blocks. The client app will have to convert the ObjcLDChangedFlag into the expected typed ObjcLDChangedFlag type. 5 | 6 | See the typed `ObjcLDClient` observeWithKey:owner:handler:, observeWithKeys:owner:handler:, and observeAllWithOwner:handler: for more details. 7 | */ 8 | @objc(LDChangedFlag) 9 | public class ObjcLDChangedFlag: NSObject { 10 | /// The changed feature flag's key 11 | @objc public let key: String 12 | /// The value from before the flag change occurred. 13 | @objc public let oldValue: ObjcLDValue 14 | /// The value after the flag change occurred. 15 | @objc public let newValue: ObjcLDValue 16 | 17 | init(_ changedFlag: LDChangedFlag) { 18 | self.key = changedFlag.key 19 | self.oldValue = ObjcLDValue(wrappedValue: changedFlag.oldValue) 20 | self.newValue = ObjcLDValue(wrappedValue: changedFlag.newValue) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDEvaluationDetail.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Structure that contains the evaluation result and additional information when evaluating a flag as a boolean. 4 | @objc(LDBoolEvaluationDetail) 5 | public final class ObjcLDBoolEvaluationDetail: NSObject { 6 | /// The value of the flag for the current context. 7 | @objc public let value: Bool 8 | /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. 9 | @objc public let variationIndex: Int 10 | /// A structure representing the main factor that influenced the resultant flag evaluation value. 11 | @objc public let reason: [String: ObjcLDValue]? 12 | 13 | internal init(value: Bool, variationIndex: Int?, reason: [String: ObjcLDValue]?) { 14 | self.value = value 15 | self.variationIndex = variationIndex ?? -1 16 | self.reason = reason 17 | } 18 | } 19 | 20 | /// Structure that contains the evaluation result and additional information when evaluating a flag as a double. 21 | @objc(LDDoubleEvaluationDetail) 22 | public final class ObjcLDDoubleEvaluationDetail: NSObject { 23 | /// The value of the flag for the current context. 24 | @objc public let value: Double 25 | /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. 26 | @objc public let variationIndex: Int 27 | /// A structure representing the main factor that influenced the resultant flag evaluation value. 28 | @objc public let reason: [String: ObjcLDValue]? 29 | 30 | internal init(value: Double, variationIndex: Int?, reason: [String: ObjcLDValue]?) { 31 | self.value = value 32 | self.variationIndex = variationIndex ?? -1 33 | self.reason = reason 34 | } 35 | } 36 | 37 | /// Structure that contains the evaluation result and additional information when evaluating a flag as an integer. 38 | @objc(LDIntegerEvaluationDetail) 39 | public final class ObjcLDIntegerEvaluationDetail: NSObject { 40 | /// The value of the flag for the current context. 41 | @objc public let value: Int 42 | /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. 43 | @objc public let variationIndex: Int 44 | /// A structure representing the main factor that influenced the resultant flag evaluation value. 45 | @objc public let reason: [String: ObjcLDValue]? 46 | 47 | internal init(value: Int, variationIndex: Int?, reason: [String: ObjcLDValue]?) { 48 | self.value = value 49 | self.variationIndex = variationIndex ?? -1 50 | self.reason = reason 51 | } 52 | } 53 | 54 | /// Structure that contains the evaluation result and additional information when evaluating a flag as a string. 55 | @objc(LDStringEvaluationDetail) 56 | public final class ObjcLDStringEvaluationDetail: NSObject { 57 | /// The value of the flag for the current context. 58 | @objc public let value: String? 59 | /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. 60 | @objc public let variationIndex: Int 61 | /// A structure representing the main factor that influenced the resultant flag evaluation value. 62 | @objc public let reason: [String: ObjcLDValue]? 63 | 64 | internal init(value: String?, variationIndex: Int?, reason: [String: ObjcLDValue]?) { 65 | self.value = value 66 | self.variationIndex = variationIndex ?? -1 67 | self.reason = reason 68 | } 69 | } 70 | 71 | /// Structure that contains the evaluation result and additional information when evaluating a flag as a JSON value. 72 | @objc(LDJSONEvaluationDetail) 73 | public final class ObjcLDJSONEvaluationDetail: NSObject { 74 | /// The value of the flag for the current context. 75 | @objc public let value: ObjcLDValue 76 | /// The index of the returned value within the flag's list of variations, or `-1` if the default was returned. 77 | @objc public let variationIndex: Int 78 | /// A structure representing the main factor that influenced the resultant flag evaluation value. 79 | @objc public let reason: [String: ObjcLDValue]? 80 | 81 | internal init(value: ObjcLDValue, variationIndex: Int?, reason: [String: ObjcLDValue]?) { 82 | self.value = value 83 | self.variationIndex = variationIndex ?? -1 84 | self.reason = reason 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDReference.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc(Reference) 4 | public final class ObjcLDReference: NSObject { 5 | var reference: Reference 6 | 7 | @objc public init(value: String) { 8 | reference = Reference(value) 9 | } 10 | 11 | // Initializer to wrap the Swift Reference into ObjcLDReference for use in 12 | // Objective-C apps. 13 | init(_ reference: Reference) { 14 | self.reference = reference 15 | } 16 | 17 | @objc public func isValid() -> Bool { reference.isValid() } 18 | 19 | @objc public func getError() -> NSError? { 20 | guard let error = reference.getError() 21 | else { return nil } 22 | 23 | return error as NSError 24 | } 25 | } 26 | 27 | @objc(ReferenceError) 28 | public final class ObjcLDReferenceError: NSObject { 29 | var error: ReferenceError 30 | 31 | // Initializer to wrap the Swift ReferenceError into ObjcLDReferenceError for use in 32 | // Objective-C apps. 33 | init(_ error: ReferenceError) { 34 | self.error = error 35 | } 36 | 37 | override public var description: String { self.error.description } 38 | } 39 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Used to represent the type of an `LDValue`. 5 | */ 6 | @objc(LDValueType) 7 | public enum ObjcLDValueType: Int { 8 | /// The value returned by `LDValue.getType()` when the represented value is a null. 9 | case null 10 | /// The value returned by `LDValue.getType()` when the represented value is a boolean. 11 | case bool 12 | /// The value returned by `LDValue.getType()` when the represented value is a number. 13 | case number 14 | /// The value returned by `LDValue.getType()` when the represented value is a string. 15 | case string 16 | /// The value returned by `LDValue.getType()` when the represented value is an array. 17 | case array 18 | /// The value returned by `LDValue.getType()` when the represented value is an object. 19 | case object 20 | } 21 | 22 | /** 23 | Bridged `LDValue` type for Objective-C. 24 | 25 | Can create instances from Objective-C with the provided `of` static functions, for example `[LDValue ofBool:YES]`. 26 | */ 27 | @objc(LDValue) 28 | public final class ObjcLDValue: NSObject { 29 | /// The Swift `LDValue` enum the instance is wrapping. 30 | public let wrappedValue: LDValue 31 | 32 | /** 33 | Create a instance of the bridging object for the given value. 34 | 35 | - parameter wrappedValue: The value to wrap. 36 | */ 37 | public init(wrappedValue: LDValue) { 38 | self.wrappedValue = wrappedValue 39 | } 40 | 41 | /// Create a new `LDValue` that represents a JSON null. 42 | @objc public static func ofNull() -> ObjcLDValue { 43 | return ObjcLDValue(wrappedValue: .null) 44 | } 45 | 46 | /// Create a new `LDValue` from a boolean value. 47 | @objc public static func of(bool: Bool) -> ObjcLDValue { 48 | return ObjcLDValue(wrappedValue: .bool(bool)) 49 | } 50 | 51 | /// Create a new `LDValue` from a numeric value. 52 | @objc public static func of(number: NSNumber) -> ObjcLDValue { 53 | return ObjcLDValue(wrappedValue: .number(number.doubleValue)) 54 | } 55 | 56 | /// Create a new `LDValue` from a string value. 57 | @objc public static func of(string: String) -> ObjcLDValue { 58 | return ObjcLDValue(wrappedValue: .string(string)) 59 | } 60 | 61 | /// Create a new `LDValue` from an array of values. 62 | @objc public static func of(array: [ObjcLDValue]) -> ObjcLDValue { 63 | return ObjcLDValue(wrappedValue: .array(array.map { $0.wrappedValue })) 64 | } 65 | 66 | /// Create a new `LDValue` object from dictionary of values. 67 | @objc public static func of(dict: [String: ObjcLDValue]) -> ObjcLDValue { 68 | return ObjcLDValue(wrappedValue: .object(dict.mapValues { $0.wrappedValue })) 69 | } 70 | 71 | /// Get the type of the value. 72 | @objc public func getType() -> ObjcLDValueType { 73 | switch wrappedValue { 74 | case .null: return .null 75 | case .bool: return .bool 76 | case .number: return .number 77 | case .string: return .string 78 | case .array: return .array 79 | case .object: return .object 80 | } 81 | } 82 | 83 | /** 84 | Get the value as a `Bool`. 85 | 86 | - returns: The contained boolean value or `NO` if the value is not a boolean. 87 | */ 88 | @objc public func boolValue() -> Bool { 89 | guard case let .bool(value) = wrappedValue 90 | else { return false } 91 | return value 92 | } 93 | 94 | /** 95 | Get the value as a `Double`. 96 | 97 | - returns: The contained double value or `0.0` if the value is not a number. 98 | */ 99 | @objc public func doubleValue() -> Double { 100 | guard case let .number(value) = wrappedValue 101 | else { return 0.0 } 102 | return value 103 | } 104 | 105 | /** 106 | Get the value as a `String`. 107 | 108 | - returns: The contained string value or the empty string if the value is not a string. 109 | */ 110 | @objc public func stringValue() -> String { 111 | guard case let .string(value) = wrappedValue 112 | else { return "" } 113 | return value 114 | } 115 | 116 | /** 117 | Get the value as an array. 118 | 119 | - returns: An array of the contained values, or the empty array if the value is not an array. 120 | */ 121 | @objc public func arrayValue() -> [ObjcLDValue] { 122 | guard case let .array(values) = wrappedValue 123 | else { return [] } 124 | return values.map { ObjcLDValue(wrappedValue: $0) } 125 | } 126 | 127 | /** 128 | Get the value as a dictionary representing the JSON object 129 | 130 | - returns: A dictionary representing the JSON object, or the empty dictionary if the value is not a dictionary. 131 | */ 132 | @objc public func dictValue() -> [String: ObjcLDValue] { 133 | guard case let .object(values) = wrappedValue 134 | else { return [:] } 135 | return values.mapValues { ObjcLDValue(wrappedValue: $0) } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | 12 | NSPrivacyCollectedDataType 13 | NSPrivacyCollectedDataTypeUserID 14 | NSPrivacyCollectedDataTypeLinked 15 | 16 | NSPrivacyCollectedDataTypeTracking 17 | 18 | NSPrivacyCollectedDataTypePurposes 19 | 20 | NSPrivacyCollectedDataTypePurposeProductPersonalization 21 | NSPrivacyCollectedDataTypePurposeAppFunctionality 22 | NSPrivacyCollectedDataTypePurposeAnalytics 23 | 24 | 25 | 26 | NSPrivacyCollectedDataType 27 | NSPrivacyCollectedDataTypeDeviceID 28 | NSPrivacyCollectedDataTypeLinked 29 | 30 | NSPrivacyCollectedDataTypeTracking 31 | 32 | NSPrivacyCollectedDataTypePurposes 33 | 34 | NSPrivacyCollectedDataTypePurposeProductPersonalization 35 | NSPrivacyCollectedDataTypePurposeAppFunctionality 36 | NSPrivacyCollectedDataTypePurposeAnalytics 37 | 38 | 39 | 40 | NSPrivacyCollectedDataType 41 | NSPrivacyCollectedDataTypeOtherDiagnosticData 42 | NSPrivacyCollectedDataTypeLinked 43 | 44 | NSPrivacyCollectedDataTypeTracking 45 | 46 | NSPrivacyCollectedDataTypePurposes 47 | 48 | NSPrivacyCollectedDataTypePurposeAnalytics 49 | 50 | 51 | 52 | NSPrivacyAccessedAPITypes 53 | 54 | 55 | NSPrivacyAccessedAPITypeReasons 56 | 57 | CA92.1 58 | 59 | NSPrivacyAccessedAPIType 60 | NSPrivacyAccessedAPICategoryUserDefaults 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/CacheConverter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // sourcery: autoMockable 4 | protocol CacheConverting { 5 | func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int) 6 | } 7 | 8 | // Cache model in SDK versions >=4.0.0 <6.0.0. Migration is not supported for earlier versions. 9 | // 10 | // [: [ 11 | // “userKey”: , 12 | // “environmentFlags”: [ 13 | // : [ 14 | // “userKey”: , 15 | // “mobileKey”: , 16 | // “featureFlags”: [ 17 | // : [ 18 | // “key”: , 19 | // “version”: , 20 | // “flagVersion”: , 21 | // “variation”: , 22 | // “value”: , 23 | // “trackEvents”: , 24 | // “debugEventsUntilDate”: , 25 | // "reason: , 26 | // "trackReason": 27 | // ] 28 | // ] 29 | // ] 30 | // ], 31 | // “lastUpdated”: 32 | // ] 33 | // ] 34 | 35 | final class CacheConverter: CacheConverting { 36 | 37 | static let latestCacheVersion = 9 38 | // The key used for storing data in the cache changed starting in v9. All caches prior to this version should be removed. 39 | static let fullHashCacheVersion = 9 40 | 41 | init() { } 42 | 43 | func convertCacheData(serviceFactory: ClientServiceCreating, keysToConvert: [MobileKey], maxCachedContexts: Int) { 44 | // Remove V5 cache data 45 | let standardDefaults = serviceFactory.makeKeyedValueCache(cacheKey: nil) 46 | standardDefaults.removeObject(forKey: "com.launchdarkly.dataManager.userEnvironments") 47 | 48 | var cachesToDelete: [String: FeatureFlagCaching] = [:] 49 | var cachesToConvert: [String: FeatureFlagCaching] = [:] 50 | keysToConvert.forEach { mobileKey in 51 | let flagCache = serviceFactory.makeFeatureFlagCache(mobileKey: mobileKey, maxCachedContexts: maxCachedContexts) 52 | 53 | guard let cacheVersionData = flagCache.keyedValueCache.data(forKey: "ld-cache-metadata") 54 | else { cachesToDelete[mobileKey] = flagCache; return } 55 | 56 | guard let cacheVersion = (try? JSONDecoder().decode([String: Int].self, from: cacheVersionData))?["version"] 57 | else { cachesToDelete[mobileKey] = flagCache; return } 58 | 59 | if cacheVersion == CacheConverter.latestCacheVersion { 60 | return 61 | } else if cacheVersion < CacheConverter.fullHashCacheVersion { 62 | cachesToDelete[mobileKey] = flagCache 63 | } else { 64 | cachesToConvert[mobileKey] = flagCache 65 | } 66 | } 67 | 68 | if let versionMetadata = try? JSONEncoder().encode(["version": CacheConverter.latestCacheVersion]) { 69 | cachesToDelete.forEach { (_, cache) in 70 | cache.keyedValueCache.removeAll() 71 | cache.keyedValueCache.set(versionMetadata, forKey: "ld-cache-metadata") 72 | } 73 | 74 | // Update cachesToConvert once we have something that needs migrating 75 | } 76 | } 77 | } 78 | 79 | extension Date { 80 | func isExpired(expirationDate: Date) -> Bool { 81 | self.stringEquivalentDate < expirationDate.stringEquivalentDate 82 | } 83 | } 84 | 85 | extension DateFormatter { 86 | /// Date formatter configured to format dates to/from the format 2018-08-13T19:06:38.123Z 87 | class var ldDateFormatter: DateFormatter { 88 | let formatter = DateFormatter() 89 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" 90 | formatter.timeZone = TimeZone(identifier: "UTC") 91 | return formatter 92 | } 93 | } 94 | 95 | extension Date { 96 | /// Date string using the format 2018-08-13T19:06:38.123Z 97 | var stringValue: String { DateFormatter.ldDateFormatter.string(from: self) } 98 | 99 | // When a date is converted to JSON, the resulting string is not as precise as the original date (only to the nearest .001s) 100 | // By converting the date to json, then back into a date, the result can be compared with any date re-inflated from json 101 | /// Date truncated to the nearest millisecond, which is the precision for string formatted dates 102 | var stringEquivalentDate: Date { stringValue.dateValue } 103 | } 104 | 105 | extension String { 106 | /// Date converted from a string using the format 2018-08-13T19:06:38.123Z 107 | var dateValue: Date { DateFormatter.ldDateFormatter.date(from: self) ?? Date() } 108 | } 109 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/ConnectionInformationStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class ConnectionInformationStore { 4 | private static let connectionInformationKey = "com.launchDarkly.ConnectionInformationStore.connectionInformationKey" 5 | 6 | static func retrieveStoredConnectionInformation() -> ConnectionInformation? { 7 | UserDefaults.standard.retrieve(object: ConnectionInformation.self, fromKey: ConnectionInformationStore.connectionInformationKey) 8 | } 9 | 10 | static func storeConnectionInformation(connectionInformation: ConnectionInformation) { 11 | UserDefaults.standard.save(customObject: connectionInformation, forKey: ConnectionInformationStore.connectionInformationKey) 12 | } 13 | } 14 | 15 | private extension UserDefaults { 16 | func save(customObject object: T, forKey key: String) { 17 | let encoder = JSONEncoder() 18 | if let encoded = try? encoder.encode(object) { 19 | self.set(encoded, forKey: key) 20 | } 21 | } 22 | 23 | func retrieve(object type: T.Type, fromKey key: String) -> T? { 24 | guard let data = self.data(forKey: key), 25 | let object = try? JSONDecoder().decode(type, from: data) 26 | else { return nil } 27 | 28 | return object 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/DiagnosticCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // sourcery: autoMockable 4 | protocol DiagnosticCaching { 5 | var lastStats: DiagnosticStats? { get } 6 | 7 | func getDiagnosticId() -> DiagnosticId 8 | func getCurrentStatsAndReset() -> DiagnosticStats 9 | func incrementDroppedEventCount() 10 | func recordEventsInLastBatch(eventsInLastBatch: Int) 11 | func addStreamInit(streamInit: DiagnosticStreamInit) 12 | } 13 | 14 | final class DiagnosticCache: DiagnosticCaching { 15 | private static let diagnosticDataKey = "com.launchdarkly.DiagnosticCache.diagnosticData" 16 | private static let cacheQueueLabel = "com.launchdarkly.DiagnosticCache.cacheQueue" 17 | 18 | private let sdkKey: String 19 | private let dataKey: String 20 | 21 | private(set) var lastStats: DiagnosticStats? 22 | 23 | private var cacheQueue = DispatchQueue(label: cacheQueueLabel) 24 | 25 | init(sdkKey: String) { 26 | self.sdkKey = sdkKey 27 | self.dataKey = "\(DiagnosticCache.diagnosticDataKey).\(sdkKey)" 28 | 29 | if let storedData = StoreData.load(from: dataKey) { 30 | let oldId = DiagnosticId(diagnosticId: storedData.instanceId, sdkKey: sdkKey) 31 | lastStats = DiagnosticStats(id: oldId, creationDate: Date().millisSince1970, dataSinceDate: storedData.dataSinceDate, droppedEvents: storedData.droppedEvents, eventsInLastBatch: storedData.eventsInLastBatch, streamInits: storedData.streamInits) 32 | } 33 | StoreData.defaultWithRandomId().save(dataKey) 34 | } 35 | 36 | func getDiagnosticId() -> DiagnosticId { 37 | let stored = cacheQueue.sync { loadOrSetup() } 38 | return DiagnosticId(diagnosticId: stored.instanceId, sdkKey: sdkKey) 39 | } 40 | 41 | func getCurrentStatsAndReset() -> DiagnosticStats { 42 | let now = Date().millisSince1970 43 | // swiftlint:disable:next implicitly_unwrapped_optional 44 | var stored: StoreData! 45 | cacheQueue.sync { 46 | stored = loadOrSetup() 47 | updateStoredData { 48 | $0.dataSinceDate = now 49 | $0.droppedEvents = 0 50 | $0.eventsInLastBatch = 0 51 | $0.streamInits = [] 52 | } 53 | } 54 | return DiagnosticStats(id: DiagnosticId(diagnosticId: stored.instanceId, sdkKey: sdkKey), 55 | creationDate: now, 56 | dataSinceDate: stored.dataSinceDate, 57 | droppedEvents: stored.droppedEvents, 58 | eventsInLastBatch: stored.eventsInLastBatch, 59 | streamInits: stored.streamInits) 60 | } 61 | 62 | func incrementDroppedEventCount() { 63 | updateStoredDataSync { $0.droppedEvents += 1 } 64 | } 65 | 66 | func recordEventsInLastBatch(eventsInLastBatch: Int) { 67 | updateStoredDataSync { $0.eventsInLastBatch = eventsInLastBatch } 68 | } 69 | 70 | func addStreamInit(streamInit: DiagnosticStreamInit) { 71 | updateStoredDataSync { $0.streamInits.append(streamInit) } 72 | } 73 | 74 | private func loadOrSetup() -> StoreData { 75 | let stored = StoreData.load(from: dataKey) 76 | if let storeData = stored { 77 | return storeData 78 | } else { 79 | let new = StoreData.defaultWithRandomId() 80 | new.save(dataKey) 81 | return new 82 | } 83 | } 84 | 85 | private func updateStoredDataSync(updateFunc: (inout StoreData) -> Void) { 86 | cacheQueue.sync { updateStoredData(updateFunc: updateFunc) } 87 | } 88 | 89 | private func updateStoredData(updateFunc: (inout StoreData) -> Void) { 90 | var storeData = StoreData.load(from: dataKey) ?? StoreData.defaultWithRandomId() 91 | updateFunc(&storeData) 92 | storeData.save(dataKey) 93 | } 94 | } 95 | 96 | private struct StoreData: Codable { 97 | let instanceId: String 98 | var dataSinceDate: Int64 99 | var droppedEvents: Int 100 | var eventsInLastBatch: Int 101 | var streamInits: [DiagnosticStreamInit] 102 | 103 | static func defaultWithRandomId() -> StoreData { 104 | StoreData(instanceId: UUID().uuidString, dataSinceDate: Date().millisSince1970, droppedEvents: 0, eventsInLastBatch: 0, streamInits: []) 105 | } 106 | 107 | static func load(from: String) -> StoreData? { 108 | let defaults = UserDefaults.standard 109 | if let storedData = defaults.data(forKey: from) { 110 | do { 111 | return try JSONDecoder().decode(self, from: storedData) 112 | } catch { 113 | defaults.removeObject(forKey: from) 114 | } 115 | } 116 | return nil 117 | } 118 | 119 | func save(_ toKey: String) { 120 | let defaults = UserDefaults.standard 121 | do { 122 | let encoded: Data = try JSONEncoder().encode(self) 123 | defaults.set(encoded, forKey: toKey) 124 | } catch {} 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/KeyedValueCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // sourcery: autoMockable 4 | protocol KeyedValueCaching { 5 | func set(_ value: Data, forKey: String) 6 | func data(forKey: String) -> Data? 7 | func dictionary(forKey: String) -> [String: Any]? 8 | func removeObject(forKey: String) 9 | func removeAll() 10 | func keys() -> [String] 11 | } 12 | 13 | extension UserDefaults: KeyedValueCaching { 14 | func set(_ value: Data, forKey: String) { 15 | set(value as Any?, forKey: forKey) 16 | } 17 | 18 | func removeAll() { 19 | dictionaryRepresentation().keys.forEach { removeObject(forKey: $0) } 20 | } 21 | 22 | func keys() -> [String] { 23 | dictionaryRepresentation().keys.map { String($0) } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/CwlSysctl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CwlSysctl.swift 3 | // CwlUtils 4 | // 5 | // Created by Matt Gallagher on 2016/02/03. 6 | // Copyright © 2016 Matt Gallagher ( http://cocoawithlove.com ). All rights reserved. 7 | // 8 | // Permission to use, copy, modify, and/or distribute this software for any 9 | // purpose with or without fee is hereby granted, provided that the above 10 | // copyright notice and this permission notice appear in all copies. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 13 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 14 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 15 | // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 16 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 17 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 18 | // IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 19 | // 20 | // LaunchDarkly Notes 21 | // This file is used on macOS to extract the device model from the system library 22 | import Foundation 23 | 24 | /// A "static"-only namespace around a series of functions that operate on buffers returned from the `Darwin.sysctl` function 25 | struct Sysctl { 26 | /// Possible errors. 27 | enum Error: Swift.Error { 28 | case unknown 29 | case malformedUTF8 30 | case posixError(POSIXErrorCode) 31 | } 32 | 33 | /// Access the raw data for an array of sysctl identifiers. 34 | static func dataForKeys(_ keys: [Int32]) throws -> [Int8] { 35 | return try keys.withUnsafeBufferPointer { keysPointer throws -> [Int8] in 36 | // Preflight the request to get the required data size 37 | var requiredSize = 0 38 | let preFlightResult = Darwin.sysctl(UnsafeMutablePointer(mutating: keysPointer.baseAddress), UInt32(keys.count), nil, &requiredSize, nil, 0) 39 | if preFlightResult != 0 { 40 | throw POSIXErrorCode(rawValue: errno).map { 41 | print($0.rawValue) 42 | return Error.posixError($0) 43 | } ?? Error.unknown 44 | } 45 | 46 | // Run the actual request with an appropriately sized array buffer 47 | let data = [Int8](repeating: 0, count: requiredSize) 48 | let result = data.withUnsafeBufferPointer { dataBuffer -> Int32 in 49 | return Darwin.sysctl(UnsafeMutablePointer(mutating: keysPointer.baseAddress), UInt32(keys.count), UnsafeMutableRawPointer(mutating: dataBuffer.baseAddress), &requiredSize, nil, 0) 50 | } 51 | if result != 0 { 52 | throw POSIXErrorCode(rawValue: errno).map { 53 | Error.posixError($0) 54 | } ?? Error.unknown 55 | } 56 | 57 | return data 58 | } 59 | } 60 | 61 | /// Invoke `sysctl` with an array of identifers, interpreting the returned buffer as a `String`. This function will throw `Error.malformedUTF8` if the buffer returned from `sysctl` cannot be interpreted as a UTF8 buffer. 62 | static func stringForKeys(_ keys: [Int32]) throws -> String { 63 | let optionalString = try dataForKeys(keys).withUnsafeBufferPointer { dataPointer -> String? in 64 | dataPointer.baseAddress.flatMap { 65 | String(validatingUTF8: $0) 66 | } 67 | } 68 | guard let str = optionalString 69 | else { 70 | throw Error.malformedUTF8 71 | } 72 | return str 73 | } 74 | 75 | /// e.g. "MacPro4,1" 76 | static var model: String { 77 | // swiftlint:disable:next force_try 78 | return try! Sysctl.stringForKeys([CTL_HW, HW_MODEL]) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | 4 | // sourcery: autoMockable 5 | protocol DiagnosticReporting { 6 | func setMode(_ runMode: LDClientRunMode, online: Bool) 7 | } 8 | 9 | class NullDiagnosticReporter: DiagnosticReporting { 10 | func setMode(_ runMode: LDClientRunMode, online: Bool) { 11 | } 12 | } 13 | 14 | class DiagnosticReporter: DiagnosticReporting { 15 | private let service: DarklyServiceProvider 16 | private let environmentReporting: EnvironmentReporting 17 | private var timer: TimeResponding? 18 | private var sentInit: Bool 19 | private let stateQueue = DispatchQueue(label: "com.launchdarkly.diagnosticReporter.state", qos: .background) 20 | private let workQueue = DispatchQueue(label: "com.launchdarkly.diagnosticReporter.work", qos: .background) 21 | 22 | init(service: DarklyServiceProvider, environmentReporting: EnvironmentReporting) { 23 | self.service = service 24 | self.environmentReporting = environmentReporting 25 | self.sentInit = false 26 | } 27 | 28 | func setMode(_ runMode: LDClientRunMode, online: Bool) { 29 | if online && runMode == .foreground { 30 | startReporting() 31 | } else { 32 | stopReporting() 33 | } 34 | } 35 | 36 | private func startReporting() { 37 | stateQueue.sync { 38 | timer?.cancel() 39 | if let cache = self.service.diagnosticCache { 40 | if !sentInit { 41 | sentInit = true 42 | if let lastStats = cache.lastStats { 43 | sendDiagnosticEventAsync(diagnosticEvent: lastStats) 44 | } 45 | let initEvent = DiagnosticInit(config: service.config, 46 | environmentReporting: environmentReporting, 47 | diagnosticId: cache.getDiagnosticId(), 48 | creationDate: Date().millisSince1970) 49 | sendDiagnosticEventAsync(diagnosticEvent: initEvent) 50 | } 51 | 52 | timer = LDTimer(withTimeInterval: service.config.diagnosticRecordingInterval, fireQueue: workQueue) { 53 | self.sendDiagnosticEventSync(diagnosticEvent: cache.getCurrentStatsAndReset()) 54 | } 55 | } 56 | } 57 | } 58 | 59 | private func stopReporting() { 60 | stateQueue.sync { 61 | timer?.cancel() 62 | timer = nil 63 | } 64 | } 65 | 66 | private func sendDiagnosticEventAsync(diagnosticEvent: T) { 67 | workQueue.async { 68 | self.sendDiagnosticEventSync(diagnosticEvent: diagnosticEvent) 69 | } 70 | } 71 | 72 | private func sendDiagnosticEventSync(diagnosticEvent: T) { 73 | os_log("%s Sending diagnostic event: %s", log: service.config.logger, type: .debug, typeName(and: #function), String(describing: diagnosticEvent)) 74 | self.service.publishDiagnostic(diagnosticEvent: diagnosticEvent) { response in 75 | let shouldRetry = self.processSendResponse(response: response.urlResponse as? HTTPURLResponse, error: response.error, isRetry: false) 76 | if shouldRetry { 77 | self.service.publishDiagnostic(diagnosticEvent: diagnosticEvent) { response in 78 | _ = self.processSendResponse(response: response.urlResponse as? HTTPURLResponse, error: response.error, isRetry: true) 79 | } 80 | } 81 | } 82 | } 83 | 84 | private func processSendResponse(response: HTTPURLResponse?, error: Error?, isRetry: Bool) -> Bool { 85 | if error == nil && (200..<300).contains(response?.statusCode ?? 0) { 86 | os_log("%s Completed sending diagnostic event.", log: service.config.logger, type: .debug, typeName) 87 | return false 88 | } 89 | 90 | if let statusCode = response?.statusCode, (400..<500).contains(statusCode) && ![400, 408, 429].contains(statusCode) { 91 | os_log("%s Dropping diagnostic event due to non-retriable response: %s", log: service.config.logger, type: .debug, typeName, String(describing: response)) 92 | return false 93 | } 94 | 95 | os_log("%s Sending diagnostic failed with error: %s response: %s", log: service.config.logger, type: .debug, 96 | typeName, 97 | String(describing: error), 98 | String(describing: response)) 99 | 100 | if isRetry { 101 | os_log("%s dropping diagnostic due to failed retry", log: service.config.logger, type: .debug, typeName) 102 | return false 103 | } 104 | 105 | return true 106 | } 107 | } 108 | 109 | extension DiagnosticReporter: TypeIdentifying { } 110 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(iOS) 4 | import UIKit 5 | #elseif os(watchOS) 6 | import WatchKit 7 | #elseif os(OSX) 8 | import AppKit 9 | #elseif os(tvOS) 10 | import UIKit 11 | #endif 12 | 13 | enum OperatingSystem: String { 14 | case iOS, watchOS, macOS, tvOS, unknown 15 | 16 | static var allOperatingSystems: [OperatingSystem] { 17 | [.iOS, .watchOS, .macOS, .tvOS] 18 | } 19 | 20 | var isBackgroundEnabled: Bool { 21 | OperatingSystem.backgroundEnabledOperatingSystems.contains(self) 22 | } 23 | static var backgroundEnabledOperatingSystems: [OperatingSystem] { 24 | [.macOS] 25 | } 26 | 27 | var isStreamingEnabled: Bool { 28 | OperatingSystem.streamingEnabledOperatingSystems.contains(self) 29 | } 30 | static var streamingEnabledOperatingSystems: [OperatingSystem] { 31 | [.iOS, .macOS, .tvOS] 32 | } 33 | } 34 | 35 | // sourcery: autoMockable 36 | protocol EnvironmentReporting { 37 | // sourcery: defaultMockValue = Constants.applicationInfo 38 | var applicationInfo: ApplicationInfo { get } 39 | // sourcery: defaultMockValue = true 40 | var isDebugBuild: Bool { get } 41 | // sourcery: defaultMockValue = Constants.deviceModel 42 | var deviceModel: String { get } 43 | // sourcery: defaultMockValue = Constants.systemVersion 44 | var systemVersion: String { get } 45 | // sourcery: defaultMockValue = Constants.vendorUUID 46 | var vendorUUID: String? { get } 47 | // sourcery: defaultMockValue = Constants.manufacturer 48 | var manufacturer: String { get } 49 | // sourcery: defaultMockValue = Constants.locale 50 | var locale: String { get } 51 | // sourcery: defaultMockValue = Constants.osFamily 52 | var osFamily: String { get } 53 | } 54 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class ApplicationInfoEnvironmentReporter: EnvironmentReporterChainBase { 4 | private var info: ApplicationInfo 5 | 6 | public init(_ applicationInfo: ApplicationInfo) { 7 | self.info = applicationInfo 8 | } 9 | 10 | override var applicationInfo: ApplicationInfo { 11 | // defer to super if applicationId is missing. 12 | if info.applicationId == nil { 13 | info = super.applicationInfo 14 | } 15 | return info 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterBuilder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class EnvironmentReporterBuilder { 4 | private var applicationInfo: ApplicationInfo? 5 | private var collectPlatformTelemetry: Bool = false 6 | 7 | /// Sets the application info that this environment reporter will report when asked in the future, overriding the automatically sourced {@link ApplicationInfo} 8 | public func applicationInfo(_ applicationInfo: ApplicationInfo) { 9 | self.applicationInfo = applicationInfo 10 | } 11 | 12 | /// Enables automatically collecting attributes from the platform. 13 | public func enableCollectionFromPlatform() { 14 | collectPlatformTelemetry = true 15 | } 16 | 17 | func build() -> EnvironmentReporting { 18 | /** 19 | * Create chain of responsibility with the following priority order 20 | * 1. {@link ApplicationInfoEnvironmentReporter} - holds customer override 21 | * 2. {@link AndroidEnvironmentReporter} - Android platform API next 22 | * 3. {@link SDKEnvironmentReporter} - Fallback is SDK constants 23 | */ 24 | var reporters: [EnvironmentReporterChainBase] = [] 25 | 26 | if let info = applicationInfo { 27 | reporters.append(ApplicationInfoEnvironmentReporter(info)) 28 | } 29 | 30 | if collectPlatformTelemetry { 31 | #if os(iOS) 32 | reporters.append(IOSEnvironmentReporter()) 33 | #elseif os(watchOS) 34 | reporters.append(WatchOSEnvironmentReporter()) 35 | #elseif os(OSX) 36 | reporters.append(MacOSEnvironmentReporter()) 37 | #elseif os(tvOS) 38 | reporters.append(TVOSEnvironmentReporter()) 39 | #endif 40 | } 41 | 42 | // always add fallback reporter 43 | reporters.append(SDKEnvironmentReporter()) 44 | 45 | // build chain of responsibility by iterating on all but last element 46 | for i in reporters.indices.dropLast() { 47 | reporters[i].setNext(reporters[i + 1]) 48 | } 49 | 50 | // guaranteed non-empty since fallback reporter is always added 51 | return reporters[0] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class EnvironmentReporterChainBase: EnvironmentReporting { 4 | 5 | private static let UNKNOWN: String = "unknown" 6 | 7 | // the next reporter in the chain if there is one 8 | private var next: EnvironmentReporterChainBase? 9 | 10 | public func setNext(_ next: EnvironmentReporterChainBase) { 11 | self.next = next 12 | } 13 | 14 | var applicationInfo: ApplicationInfo { 15 | if let n = next { 16 | return n.applicationInfo 17 | } 18 | 19 | var info = ApplicationInfo() 20 | info.applicationIdentifier(EnvironmentReporterChainBase.UNKNOWN) 21 | info.applicationVersion(EnvironmentReporterChainBase.UNKNOWN) 22 | info.applicationName(EnvironmentReporterChainBase.UNKNOWN) 23 | info.applicationVersionName(EnvironmentReporterChainBase.UNKNOWN) 24 | 25 | return info 26 | } 27 | 28 | var isDebugBuild: Bool { next?.isDebugBuild ?? false } 29 | var deviceModel: String { next?.deviceModel ?? EnvironmentReporterChainBase.UNKNOWN } 30 | var systemVersion: String { next?.systemVersion ?? EnvironmentReporterChainBase.UNKNOWN } 31 | 32 | var vendorUUID: String? { next?.vendorUUID } 33 | 34 | var manufacturer: String { next?.manufacturer ?? "Apple" } 35 | var locale: String { next?.locale ?? Locale.autoupdatingCurrent.identifier } 36 | var osFamily: String { next?.osFamily ?? "Apple" } 37 | } 38 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporter.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import Foundation 3 | import UIKit 4 | 5 | class IOSEnvironmentReporter: EnvironmentReporterChainBase { 6 | override var applicationInfo: ApplicationInfo { 7 | var info = ApplicationInfo() 8 | info.applicationIdentifier(Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String) 9 | info.applicationVersion(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) 10 | info.applicationName(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) 11 | info.applicationVersionName(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) 12 | 13 | // defer to super if applicationId is missing. This logic is after the setter since the setter has built in sanitization 14 | if info.applicationId == nil { 15 | info = super.applicationInfo 16 | } 17 | return info 18 | } 19 | 20 | override var deviceModel: String { UIDevice.current.model } 21 | override var systemVersion: String { UIDevice.current.systemVersion } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/MacOSEnvironmentReporter.swift: -------------------------------------------------------------------------------- 1 | #if os(OSX) 2 | import Foundation 3 | import AppKit 4 | 5 | class MacOSEnvironmentReporter: EnvironmentReporterChainBase { 6 | override var applicationInfo: ApplicationInfo { 7 | var info = ApplicationInfo() 8 | info.applicationIdentifier(Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String) 9 | info.applicationVersion(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) 10 | info.applicationName(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) 11 | info.applicationVersionName(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) 12 | 13 | // defer to super if applicationId is missing. This logic is after the setter since the setter has built in sanitization 14 | if info.applicationId == nil { 15 | info = super.applicationInfo 16 | } 17 | return info 18 | } 19 | 20 | override var deviceModel: String { Sysctl.modelWithoutVersion } 21 | override var systemVersion: String { ProcessInfo.processInfo.operatingSystemVersion.compactVersionString } 22 | } 23 | 24 | extension OperatingSystemVersion { 25 | var compactVersionString: String { 26 | "\(majorVersion).\(minorVersion).\(patchVersion)" 27 | } 28 | } 29 | 30 | extension Sysctl { 31 | static var modelWithoutVersion: String { 32 | // swiftlint:disable:next force_try 33 | let modelRegex = try! NSRegularExpression(pattern: "([A-Za-z]+)\\d{1,2},\\d") 34 | let model = Sysctl.model // e.g. "MacPro4,1" 35 | return modelRegex.firstCaptureGroup(in: model, options: [], range: model.range) ?? "mac" 36 | } 37 | } 38 | 39 | private extension String { 40 | func substring(_ range: NSRange) -> String? { 41 | guard range.location >= 0 && range.location < self.count, 42 | range.location + range.length >= 0 && range.location + range.length < self.count 43 | else { return nil } 44 | let startIndex = index(self.startIndex, offsetBy: range.location) 45 | let endIndex = index(self.startIndex, offsetBy: range.length) 46 | return String(self[startIndex.. String? { 56 | guard let match = self.firstMatch(in: string, options: [], range: string.range), 57 | let group = string.substring(match.range(at: 1)) 58 | else { return nil } 59 | return group 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ReportingConsts.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ReportingConsts { 4 | static let sdkVersion = "9.13.0" // x-release-please-version 5 | static let sdkName = "ios-client-sdk" 6 | } 7 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(iOS) || os(tvOS) 4 | import UIKit 5 | #endif 6 | 7 | class SDKEnvironmentReporter: EnvironmentReporterChainBase { 8 | override var applicationInfo: ApplicationInfo { 9 | var info = ApplicationInfo() 10 | info.applicationIdentifier(ReportingConsts.sdkName) 11 | info.applicationVersion(ReportingConsts.sdkVersion) 12 | info.applicationName(ReportingConsts.sdkName) 13 | info.applicationVersionName(ReportingConsts.sdkVersion) 14 | return info 15 | } 16 | 17 | override var isDebugBuild: Bool { 18 | #if DEBUG 19 | return true 20 | #else 21 | return false 22 | #endif 23 | } 24 | 25 | #if os(iOS) 26 | override var vendorUUID: String? { UIDevice.current.identifierForVendor?.uuidString } 27 | #elseif os(watchOS) 28 | override var vendorUUID: String? { nil } 29 | #elseif os(OSX) 30 | override var vendorUUID: String? { nil } 31 | #elseif os(tvOS) 32 | override var vendorUUID: String? { UIDevice.current.identifierForVendor?.uuidString } 33 | #endif 34 | } 35 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SystemCapabilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(iOS) || os(tvOS) 4 | import UIKit 5 | #elseif os(OSX) 6 | import AppKit 7 | #elseif os(watchOS) 8 | import WatchKit 9 | #endif 10 | 11 | class SystemCapabilities { 12 | #if os(iOS) 13 | static var backgroundNotification: Notification.Name? { UIApplication.didEnterBackgroundNotification } 14 | static var foregroundNotification: Notification.Name? { UIApplication.willEnterForegroundNotification } 15 | static var systemName: String { UIDevice.current.systemName } 16 | static var operatingSystem: OperatingSystem { .iOS } 17 | #elseif os(watchOS) 18 | static var backgroundNotification: Notification.Name? { nil } 19 | static var foregroundNotification: Notification.Name? { nil } 20 | static var systemName: String { WKInterfaceDevice.current().systemName } 21 | static var operatingSystem: OperatingSystem { .watchOS } 22 | #elseif os(OSX) 23 | static var backgroundNotification: Notification.Name? { NSApplication.willResignActiveNotification } 24 | static var foregroundNotification: Notification.Name? { NSApplication.didBecomeActiveNotification } 25 | static var systemName: String { "macOS" } 26 | static var operatingSystem: OperatingSystem { .macOS } 27 | #elseif os(tvOS) 28 | static var backgroundNotification: Notification.Name? { UIApplication.didEnterBackgroundNotification } 29 | static var foregroundNotification: Notification.Name? { UIApplication.willEnterForegroundNotification } 30 | static var systemName: String { UIDevice.current.systemName } 31 | static var operatingSystem: OperatingSystem { .tvOS } 32 | #endif 33 | } 34 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/TVOSEnvironmentReporter.swift: -------------------------------------------------------------------------------- 1 | #if os(tvOS) 2 | import Foundation 3 | import UIKit 4 | 5 | class TVOSEnvironmentReporter: EnvironmentReporterChainBase { 6 | override var applicationInfo: ApplicationInfo { 7 | var info = ApplicationInfo() 8 | info.applicationIdentifier(Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String) 9 | info.applicationVersion(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) 10 | info.applicationName(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) 11 | info.applicationVersionName(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) 12 | 13 | // defer to super if applicationId is missing. This logic is after the setter since the setter has built in sanitization 14 | if info.applicationId == nil { 15 | info = super.applicationInfo 16 | } 17 | return info 18 | } 19 | 20 | override var deviceModel: String { UIDevice.current.model } 21 | override var systemVersion: String { UIDevice.current.systemVersion } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporter.swift: -------------------------------------------------------------------------------- 1 | #if os(watchOS) 2 | import Foundation 3 | import WatchKit 4 | 5 | class WatchOSEnvironmentReporter: EnvironmentReporterChainBase { 6 | override var applicationInfo: ApplicationInfo { 7 | var info = ApplicationInfo() 8 | info.applicationIdentifier(Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String) 9 | info.applicationVersion(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) 10 | info.applicationName(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) 11 | info.applicationVersionName(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) 12 | 13 | // defer to super if applicationId is missing. This logic is after the setter since the setter has built in sanitization 14 | if info.applicationId == nil { 15 | info = super.applicationInfo 16 | } 17 | return info 18 | } 19 | 20 | override var deviceModel: String { WKInterfaceDevice.current().model } 21 | override var systemVersion: String { WKInterfaceDevice.current().systemVersion } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/LDTimer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol TimeResponding { 4 | var fireDate: Date? { get } 5 | 6 | init(withTimeInterval: TimeInterval, fireQueue: DispatchQueue, fireAt: Date?, execute: @escaping () -> Void) 7 | func cancel() 8 | } 9 | 10 | final class LDTimer: TimeResponding { 11 | 12 | private (set) weak var timer: Timer? 13 | private let fireQueue: DispatchQueue 14 | private let execute: () -> Void 15 | private (set) var isCancelled: Bool = false 16 | var fireDate: Date? { timer?.fireDate } 17 | 18 | init(withTimeInterval timeInterval: TimeInterval, fireQueue: DispatchQueue = DispatchQueue.main, fireAt: Date? = nil, execute: @escaping () -> Void) { 19 | self.fireQueue = fireQueue 20 | self.execute = execute 21 | 22 | // the run loop retains the timer, so the property is weak to avoid a retain cycle. Setting the timer to a strong reference is important so that the timer doesn't get nil'd before it's added to the run loop. 23 | let timer: Timer 24 | if let at = fireAt { 25 | timer = Timer(fireAt: at, interval: timeInterval, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) 26 | } else { 27 | timer = Timer(timeInterval: timeInterval, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true) 28 | } 29 | self.timer = timer 30 | RunLoop.main.add(timer, forMode: RunLoop.Mode.default) 31 | } 32 | 33 | deinit { 34 | timer?.invalidate() 35 | } 36 | 37 | @objc private func timerFired() { 38 | fireQueue.async { [weak self] in 39 | guard (self?.isCancelled ?? true) == false 40 | else { return } 41 | self?.execute() 42 | } 43 | } 44 | 45 | func cancel() { 46 | timer?.invalidate() 47 | isCancelled = true 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/Log.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol TypeIdentifying { } 4 | 5 | extension TypeIdentifying { 6 | var typeName: String { 7 | String(describing: type(of: self)) 8 | } 9 | 10 | func typeName(and method: String) -> String { 11 | typeName + "." + method 12 | } 13 | 14 | static var typeName: String { 15 | String(describing: self) 16 | } 17 | 18 | static func typeName(and method: String) -> String { 19 | typeName + "." + method 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/NetworkReporter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(SystemConfiguration) 3 | import SystemConfiguration 4 | #endif 5 | 6 | class NetworkReporter { 7 | #if canImport(SystemConfiguration) 8 | // Sourced from: https://stackoverflow.com/a/39782859 9 | static func isConnectedToNetwork() -> Bool { 10 | var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) 11 | zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) 12 | zeroAddress.sin_family = sa_family_t(AF_INET) 13 | 14 | let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) { 15 | $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in 16 | SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) 17 | } 18 | } 19 | 20 | var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0) 21 | if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false { 22 | return false 23 | } 24 | 25 | let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 26 | let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 27 | let reachability = (isReachable && !needsConnection) 28 | 29 | return reachability 30 | } 31 | #else 32 | static func isConnectedToNetwork() -> Bool { true } 33 | #endif 34 | } 35 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/SheddingQueue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum TaskResult { 4 | case complete 5 | case error 6 | case shed 7 | } 8 | 9 | typealias TaskHandlerCompletion = () -> Void 10 | typealias TaskHandler = (_ completion: @escaping TaskHandlerCompletion) -> Void 11 | typealias TaskCompletion = (_ result: TaskResult) -> Void 12 | 13 | struct Task { 14 | let work: TaskHandler 15 | let sheddable: Bool 16 | let completion: TaskCompletion 17 | } 18 | 19 | class SheddingQueue { 20 | private let stateQueue: DispatchQueue = DispatchQueue(label: "StateQueue") 21 | private let identifyQueue: DispatchQueue = DispatchQueue(label: "IdentifyQueue") 22 | 23 | private var inFlight: Task? 24 | private var queue: [Task] = [] 25 | 26 | func enqueue(request: Task) { 27 | stateQueue.async { [self] in 28 | guard inFlight != nil else { 29 | inFlight = request 30 | identifyQueue.async { self.execute() } 31 | return 32 | } 33 | 34 | if let lastTask = queue.last, lastTask.sheddable { 35 | queue.removeLast() 36 | lastTask.completion(.shed) 37 | } 38 | 39 | queue.append(request) 40 | } 41 | } 42 | 43 | private func execute() { 44 | var nextTask: Task? 45 | 46 | stateQueue.sync { 47 | nextTask = inFlight 48 | } 49 | 50 | if nextTask == nil { 51 | return 52 | } 53 | 54 | guard let request = nextTask else { return } 55 | 56 | request.work() { [self] in 57 | request.completion(.complete) 58 | 59 | stateQueue.sync { 60 | inFlight = queue.first 61 | if inFlight != nil { 62 | queue.remove(at: 0) 63 | identifyQueue.async { self.execute() } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | 4 | typealias RunClosure = () -> Void 5 | 6 | // sourcery: autoMockable 7 | protocol Throttling { 8 | func runThrottled(_ runClosure: @escaping RunClosure) 9 | func cancelThrottledRun() 10 | } 11 | 12 | final class Throttler: Throttling { 13 | struct Constants { 14 | static let defaultDelay: TimeInterval = 60.0 15 | fileprivate static let runQueueName = "LaunchDarkly.Throttler.runQueue" 16 | } 17 | 18 | // Exposed to let tests keep tsan happy 19 | let runQueue = DispatchQueue(label: Constants.runQueueName, qos: .userInitiated) 20 | let dispatcher: ((@escaping RunClosure) -> Void) 21 | let throttlingEnabled: Bool 22 | let maxDelay: TimeInterval 23 | private let logger: OSLog 24 | 25 | private (set) var runAttempts = -1 26 | private (set) var workItem: DispatchWorkItem? 27 | 28 | init( 29 | logger: OSLog, 30 | maxDelay: TimeInterval = Constants.defaultDelay, 31 | isDebugBuild: Bool = false, 32 | dispatcher: ((@escaping RunClosure) -> Void)? = nil) { 33 | self.logger = logger 34 | self.throttlingEnabled = !isDebugBuild 35 | self.maxDelay = maxDelay 36 | self.dispatcher = dispatcher ?? { DispatchQueue.global(qos: .userInitiated).async(execute: $0) } 37 | } 38 | 39 | func runThrottled(_ runClosure: @escaping RunClosure) { 40 | if let logMsg = runThrottledSync(runClosure) { 41 | os_log("%s", log: self.logger, type: .debug, typeName(and: #function), logMsg) 42 | } 43 | } 44 | 45 | func runThrottledSync(_ runClosure: @escaping RunClosure) -> String? { 46 | if !throttlingEnabled { 47 | dispatcher(runClosure) 48 | return typeName(and: #function) + "Executing run closure unthrottled, as throttling is disabled." 49 | } 50 | 51 | return runQueue.sync { 52 | runAttempts += 1 53 | 54 | let resetDelay = min(maxDelay, TimeInterval(pow(2.0, Double(runAttempts - 1)))) 55 | runQueue.asyncAfter(deadline: .now() + resetDelay) { [weak self] in 56 | guard let self = self else { return } 57 | self.runAttempts = max(0, self.runAttempts - 1) 58 | } 59 | 60 | if runAttempts <= 1 { 61 | dispatcher(runClosure) 62 | return typeName(and: #function) + "Executing run closure unthrottled." 63 | } 64 | 65 | let jittered = resetDelay / 2 + Double.random(in: 0.0...(resetDelay / 2)) 66 | let workItem = DispatchWorkItem { [weak self] in 67 | guard let self = self else { return } 68 | self.dispatcher(runClosure) 69 | self.workItem = nil 70 | } 71 | self.workItem?.cancel() 72 | self.workItem = workItem 73 | runQueue.asyncAfter(deadline: .now() + jittered, execute: workItem) 74 | return typeName(and: #function) + "Throttling run closure. Run attempts: \(runAttempts), Delay: \(jittered)" 75 | } 76 | } 77 | 78 | func cancelThrottledRun() { 79 | runQueue.sync { 80 | self.workItem?.cancel() 81 | self.workItem = nil 82 | } 83 | } 84 | } 85 | 86 | extension Throttler: TypeIdentifying { } 87 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Support/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2018 Catamorphic Co. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Support/LaunchDarkly.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for Darkly. 4 | FOUNDATION_EXPORT double LaunchDarklyVersionNumber; 5 | 6 | //! Project version string for Darkly. 7 | FOUNDATION_EXPORT const unsigned char LaunchDarklyVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarkly/Util.swift: -------------------------------------------------------------------------------- 1 | import CommonCrypto 2 | import Foundation 3 | 4 | class Util { 5 | internal static let validKindCharacterSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") 6 | internal static let validTagCharacterSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") 7 | 8 | class func sha256base64(_ str: String) -> String { 9 | sha256(str).base64EncodedString() 10 | } 11 | 12 | class func sha256(_ str: String) -> Data { 13 | let data = Data(str.utf8) 14 | var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) 15 | data.withUnsafeBytes { 16 | _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &digest) 17 | } 18 | return Data(digest) 19 | } 20 | } 21 | 22 | extension String { 23 | func onlyContainsCharset(_ set: CharacterSet) -> Bool { 24 | if description.rangeOfCharacter(from: set.inverted) != nil { 25 | return false 26 | } 27 | 28 | return true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - cyclomatic_complexity 3 | - function_body_length 4 | - force_cast 5 | - force_try 6 | - file_length 7 | - identifier_name 8 | - large_tuple 9 | - redundant_optional_initialization 10 | - type_body_length 11 | # Disabled opt-in rules from .swiftlint.yml in the project root. 12 | - implicitly_unwrapped_optional 13 | - let_var_whitespace 14 | - missing_docs 15 | - trailing_closure 16 | 17 | opt_in_rules: 18 | 19 | excluded: 20 | 21 | reporter: "xcode" -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Extensions/ThreadSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Quick 3 | import Nimble 4 | @testable import LaunchDarkly 5 | 6 | final class ThreadSpec: QuickSpec { 7 | override func spec() { 8 | performOnMainSpec() 9 | } 10 | 11 | private func performOnMainSpec() { 12 | var runCount = 0 13 | var ranOnMainThread = false 14 | describe("performOnMain") { 15 | context("when on the main thread") { 16 | beforeEach { 17 | runCount = 0 18 | ranOnMainThread = false 19 | Thread.performOnMain { 20 | runCount += 1 21 | ranOnMainThread = Thread.isMainThread 22 | } 23 | } 24 | it("executes the closure synchronously on the main thread") { 25 | expect(runCount) == 1 26 | expect(ranOnMainThread) == true 27 | } 28 | } 29 | context("when off the main thread") { 30 | var backgroundQueue: DispatchQueue! 31 | beforeEach { 32 | runCount = 0 33 | ranOnMainThread = false 34 | backgroundQueue = DispatchQueue(label: "com.launchdarkly.tests.ThreadSpec.backgroundQueue", qos: .utility) 35 | waitUntil { done in 36 | backgroundQueue.async { 37 | Thread.performOnMain { 38 | runCount += 1 39 | ranOnMainThread = Thread.isMainThread 40 | done() 41 | } 42 | } 43 | } 44 | } 45 | it("executes the closure synchronously on the main thread") { 46 | expect(runCount) == 1 47 | expect(ranOnMainThread) == true 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/LDClientHookSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | import Quick 4 | import Nimble 5 | import LDSwiftEventSource 6 | import XCTest 7 | @testable import LaunchDarkly 8 | 9 | final class LDClientHookSpec: XCTestCase { 10 | func testRegistration() { 11 | var count = 0 12 | let hook = MockHook(before: { _, data in count += 1; return data }, after: { _, data, _ in count += 2; return data }) 13 | var config = LDConfig(mobileKey: "mobile-key", autoEnvAttributes: .disabled) 14 | config.hooks = [hook] 15 | var testContext: TestContext! 16 | waitUntil { done in 17 | testContext = TestContext(newConfig: config) 18 | testContext.start(completion: done) 19 | } 20 | _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) 21 | XCTAssertEqual(count, 3) 22 | } 23 | 24 | func testEvaluationOrder() { 25 | var callRecord: [String] = [] 26 | let firstHook = MockHook(before: { _, data in callRecord.append("first before"); return data }, after: { _, data, _ in callRecord.append("first after"); return data }) 27 | let secondHook = MockHook(before: { _, data in callRecord.append("second before"); return data }, after: { _, data, _ in callRecord.append("second after"); return data }) 28 | var config = LDConfig(mobileKey: "mobile-key", autoEnvAttributes: .disabled) 29 | config.hooks = [firstHook, secondHook] 30 | 31 | var testContext: TestContext! 32 | waitUntil { done in 33 | testContext = TestContext(newConfig: config) 34 | testContext.start(completion: done) 35 | } 36 | 37 | _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) 38 | XCTAssertEqual(callRecord.count, 4) 39 | XCTAssertEqual(callRecord[0], "first before") 40 | XCTAssertEqual(callRecord[1], "second before") 41 | XCTAssertEqual(callRecord[2], "second after") 42 | XCTAssertEqual(callRecord[3], "first after") 43 | } 44 | 45 | func testEvaluationDetailIsCaptured() { 46 | var detail: LDEvaluationDetail? = nil 47 | let hook = MockHook(before: { _, data in return data }, after: { _, data, d in detail = d; return data }) 48 | var config = LDConfig(mobileKey: "mobile-key", autoEnvAttributes: .disabled) 49 | config.hooks = [hook] 50 | 51 | var testContext: TestContext! 52 | waitUntil { done in 53 | testContext = TestContext(newConfig: config) 54 | testContext.start(completion: done) 55 | } 56 | 57 | testContext.flagStoreMock.replaceStore(newStoredItems: FlagMaintainingMock.stubStoredItems()) 58 | _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) 59 | 60 | guard let det = detail 61 | else { 62 | fail("Details were never set by closure.") 63 | return 64 | } 65 | 66 | XCTAssertEqual(det.value, true) 67 | XCTAssertEqual(det.variationIndex, 2) 68 | } 69 | 70 | func testBeforeHookPassesDataToAfterHook() { 71 | var seriesData: EvaluationSeriesData? = nil 72 | let beforeHook: BeforeHook = { _, seriesData in 73 | var modified = seriesData 74 | modified["before"] = "was called" 75 | 76 | return modified 77 | } 78 | let hook = MockHook(before: beforeHook, after: { _, sd, _ in seriesData = sd; return sd }) 79 | var config = LDConfig(mobileKey: "mobile-key", autoEnvAttributes: .disabled) 80 | config.hooks = [hook] 81 | 82 | var testContext: TestContext! 83 | waitUntil { done in 84 | testContext = TestContext(newConfig: config) 85 | testContext.start(completion: done) 86 | } 87 | 88 | testContext.flagStoreMock.replaceStore(newStoredItems: FlagMaintainingMock.stubStoredItems()) 89 | _ = testContext.subject.boolVariation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) 90 | 91 | guard let data = seriesData 92 | else { 93 | fail("seriesData was never set by closure.") 94 | return 95 | } 96 | 97 | XCTAssertEqual(data["before"] as! String, "was called") 98 | } 99 | 100 | typealias BeforeHook = (_: EvaluationSeriesContext, _: EvaluationSeriesData) -> EvaluationSeriesData 101 | typealias AfterHook = (_: EvaluationSeriesContext, _: EvaluationSeriesData, _: LDEvaluationDetail) -> EvaluationSeriesData 102 | 103 | class MockHook: Hook { 104 | let before: BeforeHook 105 | let after: AfterHook 106 | 107 | init(before: @escaping BeforeHook, after: @escaping AfterHook) { 108 | self.before = before 109 | self.after = after 110 | } 111 | 112 | func metadata() -> LaunchDarkly.Metadata { 113 | return Metadata(name: "counting-hook") 114 | } 115 | 116 | func beforeEvaluation(seriesContext: LaunchDarkly.EvaluationSeriesContext, seriesData: LaunchDarkly.EvaluationSeriesData) -> LaunchDarkly.EvaluationSeriesData { 117 | return self.before(seriesContext, seriesData) 118 | } 119 | 120 | func afterEvaluation(seriesContext: LaunchDarkly.EvaluationSeriesContext, seriesData: LaunchDarkly.EvaluationSeriesData, evaluationDetail: LaunchDarkly.LDEvaluationDetail) -> LaunchDarkly.EvaluationSeriesData { 121 | return self.after(seriesContext, seriesData, evaluationDetail) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @testable import LaunchDarkly 4 | 5 | extension EnvironmentReportingMock { 6 | struct Constants { 7 | static let applicationInfo: ApplicationInfo = { 8 | var applicationInfo = LaunchDarkly.ApplicationInfo() 9 | applicationInfo.applicationIdentifier("idStub") 10 | applicationInfo.applicationVersion("versionStub") 11 | applicationInfo.applicationName("nameStub") 12 | applicationInfo.applicationVersionName("versionNameStub") 13 | return applicationInfo 14 | }() 15 | static let deviceModel = "deviceModelStub" 16 | static let systemVersion = "systemVersionStub" 17 | static let systemName = "systemNameStub" 18 | static let vendorUUID = "vendorUUIDStub" 19 | static let sdkVersion = "sdkVersionStub" 20 | static let manufacturer = "manufacturerStub" 21 | static let locale = "localeStub" 22 | static let osFamily = "osFamilyStub" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | @testable import LaunchDarkly 4 | 5 | final class FlagMaintainingMock: FlagMaintaining { 6 | let innerStore: FlagStore 7 | 8 | init() { 9 | innerStore = FlagStore(logger: OSLog(subsystem: "com.launchdarkly", category: "tests")) 10 | } 11 | 12 | init(storedItems: StoredItems) { 13 | innerStore = FlagStore(logger: OSLog(subsystem: "com.launchdarkly", category: "tests"), storedItems: storedItems) 14 | } 15 | 16 | var storedItems: StoredItems { 17 | innerStore.storedItems 18 | } 19 | 20 | var replaceStoreCallCount = 0 21 | var replaceStoreReceivedNewFlags: StoredItems? 22 | func replaceStore(newStoredItems: StoredItems) { 23 | replaceStoreCallCount += 1 24 | replaceStoreReceivedNewFlags = newStoredItems 25 | innerStore.replaceStore(newStoredItems: newStoredItems) 26 | } 27 | 28 | var updateStoreCallCount = 0 29 | var updateStoreReceivedUpdatedFlag: FeatureFlag? 30 | func updateStore(updatedFlag: FeatureFlag) { 31 | updateStoreCallCount += 1 32 | updateStoreReceivedUpdatedFlag = updatedFlag 33 | innerStore.updateStore(updatedFlag: updatedFlag) 34 | } 35 | 36 | var deleteFlagCallCount = 0 37 | var deleteFlagReceivedDeleteResponse: DeleteResponse? 38 | func deleteFlag(deleteResponse: DeleteResponse) { 39 | deleteFlagCallCount += 1 40 | deleteFlagReceivedDeleteResponse = deleteResponse 41 | innerStore.deleteFlag(deleteResponse: deleteResponse) 42 | } 43 | 44 | func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? { 45 | innerStore.featureFlag(for: flagKey) 46 | } 47 | 48 | static func stubStoredItems() -> StoredItems { 49 | let flags = DarklyServiceMock.Constants.stubFeatureFlags() 50 | var storedItems = StoredItems(items: flags) 51 | storedItems["userKey"] = .item(FeatureFlag(flagKey: "userKey", 52 | value: .string(UUID().uuidString), 53 | variation: DarklyServiceMock.Constants.variation, 54 | version: DarklyServiceMock.Constants.version, 55 | flagVersion: DarklyServiceMock.Constants.flagVersion, 56 | trackEvents: true, 57 | debugEventsUntilDate: Date().addingTimeInterval(30.0), 58 | reason: DarklyServiceMock.Constants.reason, 59 | trackReason: false)) 60 | return storedItems 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Mocks/LDConfigStub.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import LaunchDarkly 3 | 4 | extension LDConfig { 5 | 6 | struct Constants { 7 | static let mockMobileKey = "mockMobileKey" 8 | static let alternateMobileKey = "alternateMobileKey" 9 | } 10 | 11 | static var stub: LDConfig { 12 | stub(mobileKey: Constants.mockMobileKey, autoEnvAttributes: .disabled, isDebugBuild: true) 13 | } 14 | 15 | static func stub(mobileKey: String, autoEnvAttributes: AutoEnvAttributes, isDebugBuild: Bool) -> LDConfig { 16 | var config = LDConfig(mobileKey: mobileKey, autoEnvAttributes: autoEnvAttributes, isDebugBuild: isDebugBuild) 17 | config.baseUrl = DarklyServiceMock.Constants.mockBaseUrl 18 | config.eventsUrl = DarklyServiceMock.Constants.mockEventsUrl 19 | config.streamUrl = DarklyServiceMock.Constants.mockStreamUrl 20 | 21 | config.flagPollingInterval = 1.0 22 | 23 | return config 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Mocks/LDContextStub.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import LaunchDarkly 3 | 4 | extension LDContext { 5 | struct StubConstants { 6 | static let key: LDValue = "stub.context.key" 7 | 8 | static let name = "stub.context.name" 9 | static let isAnonymous = false 10 | 11 | static let firstName: LDValue = "stub.context.firstName" 12 | static let lastName: LDValue = "stub.context.lastName" 13 | static let country: LDValue = "stub.context.country" 14 | static let ipAddress: LDValue = "stub.context.ipAddress" 15 | static let email: LDValue = "stub.context@email.com" 16 | static let avatar: LDValue = "stub.context.avatar" 17 | static let custom: [String: LDValue] = ["stub.context.custom.keyA": "stub.context.custom.valueA", 18 | "stub.context.custom.keyB": true, 19 | "stub.context.custom.keyC": 1027, 20 | "stub.context.custom.keyD": 2.71828, 21 | "stub.context.custom.keyE": [0, 1, 2], 22 | "stub.context.custom.keyF": ["1": 1, "2": 2, "3": 3]] 23 | } 24 | 25 | static func stub(key: String? = nil, 26 | environmentReporter: EnvironmentReportingMock? = nil) -> LDContext { 27 | var builder = LDContextBuilder(key: key ?? UUID().uuidString) 28 | 29 | builder.name(StubConstants.name) 30 | builder.anonymous(StubConstants.isAnonymous) 31 | 32 | builder.trySetValue("firstName", StubConstants.firstName) 33 | builder.trySetValue("lastName", StubConstants.lastName) 34 | builder.trySetValue("country", StubConstants.country) 35 | builder.trySetValue("ip", StubConstants.ipAddress) 36 | builder.trySetValue("email", StubConstants.email) 37 | builder.trySetValue("avatar", StubConstants.avatar) 38 | 39 | for (key, value) in StubConstants.custom { 40 | builder.trySetValue(key, value) 41 | } 42 | 43 | var context: LDContext? = nil 44 | if case .success(let ctx) = builder.build() { 45 | context = ctx 46 | } 47 | 48 | return context! 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Mocks/LDEventSourceMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import LDSwiftEventSource 3 | @testable import LaunchDarkly 4 | 5 | extension EventHandler { 6 | func send(event: String, string: String) { 7 | onMessage(eventType: event, messageEvent: MessageEvent(data: string)) 8 | } 9 | 10 | func sendPing() { 11 | onMessage(eventType: "ping", messageEvent: MessageEvent(data: "")) 12 | } 13 | 14 | func sendUnauthorizedError() { 15 | onError(error: UnsuccessfulResponseError(responseCode: HTTPURLResponse.StatusCodes.unauthorized)) 16 | } 17 | 18 | func sendServerError() { 19 | onError(error: UnsuccessfulResponseError(responseCode: HTTPURLResponse.StatusCodes.internalServerError)) 20 | } 21 | 22 | func sendNonResponseError() { 23 | onError(error: DummyError()) 24 | } 25 | } 26 | 27 | class EventHandlerMock: EventHandler { 28 | var onOpenedCallCount = 0 29 | func onOpened() { 30 | onOpenedCallCount += 1 31 | } 32 | 33 | var onClosedCallCount = 0 34 | func onClosed() { 35 | onClosedCallCount += 1 36 | } 37 | 38 | var onMessageCallCount = 0 39 | var onMessageReceivedArguments: (event: String, messageEvent: MessageEvent)? 40 | func onMessage(eventType: String, messageEvent: MessageEvent) { 41 | onMessageCallCount += 1 42 | onMessageReceivedArguments = (eventType, messageEvent) 43 | } 44 | 45 | var onCommentCallCount = 0 46 | var onCommentReceivedComment: String? 47 | func onComment(comment: String) { 48 | onCommentCallCount += 1 49 | onCommentReceivedComment = comment 50 | } 51 | 52 | var onErrorCallCount = 0 53 | var onErrorReceivedError: Error? 54 | func onError(error: Error) { 55 | onErrorCallCount += 1 56 | onErrorReceivedError = error 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Models/Context/KindSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import LaunchDarkly 5 | 6 | final class KindSpec: XCTestCase { 7 | func testKindCorrectlyIdentifiesAsMulti() { 8 | let options: [(Kind, Bool)] = [ 9 | (.user, false), 10 | (.multi, true), 11 | (.custom("multi"), true), 12 | (.custom("org"), false) 13 | ] 14 | 15 | for (kind, isMulti) in options { 16 | XCTAssertEqual(kind.isMulti(), isMulti) 17 | } 18 | } 19 | 20 | func testKindCorrectlyIdentifiesAsUser() { 21 | let options: [(Kind, Bool)] = [ 22 | (.user, true), 23 | (.multi, false), 24 | (.custom(""), true), 25 | (.custom("user"), true), 26 | (.custom("org"), false) 27 | ] 28 | 29 | for (kind, isUser) in options { 30 | XCTAssertEqual(kind.isUser(), isUser) 31 | } 32 | } 33 | 34 | func testKindBuildsFromStringCorrectly() { 35 | XCTAssertNil(Kind("kind")) 36 | XCTAssertNil(Kind("no spaces allowed")) 37 | XCTAssertNil(Kind("#invalidcharactersarefun")) 38 | 39 | XCTAssertEqual(Kind(""), .user) 40 | XCTAssertEqual(Kind("user"), .user) 41 | XCTAssertEqual(Kind("User"), .custom("User")) 42 | 43 | XCTAssertEqual(Kind("multi"), .multi) 44 | XCTAssertEqual(Kind("org"), .custom("org")) 45 | } 46 | 47 | func testKindCanEncodeAndDecodeAppropriately() throws { 48 | // I know it seems silly to have these test cases be arrays instead of 49 | // simple strings. However, if I can please kindly direct your 50 | // attention to https://github.com/apple/swift-corelibs-foundation/issues/4402 51 | // you will see that older versions had an issue encoding and decoding JSON 52 | // fragments like simple strings. 53 | // 54 | // Using an array like this is a simple but effective workaround. 55 | let testCases = [ 56 | ("[\"user\"]", Kind("user"), true, false), 57 | ("[\"multi\"]", Kind("multi"), false, true), 58 | ("[\"org\"]", Kind("org"), false, false) 59 | ] 60 | 61 | for (json, expectedKind, isUser, isMulti) in testCases { 62 | let kindJson = Data(json.utf8) 63 | let kinds = try JSONDecoder().decode([Kind].self, from: kindJson) 64 | 65 | XCTAssertEqual(expectedKind, kinds[0]) 66 | XCTAssertEqual(isUser, kinds[0].isUser()) 67 | XCTAssertEqual(isMulti, kinds[0].isMulti()) 68 | 69 | try XCTAssertEqual(kindJson, JSONEncoder().encode(kinds)) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Models/Context/ReferenceSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import LaunchDarkly 5 | 6 | final class ReferenceSpec: XCTestCase { 7 | func testVerifyEquality() { 8 | let tests: [(Reference, Reference, Bool)] = [ 9 | (Reference("name"), Reference("name"), true), 10 | (Reference("name"), Reference("/name"), true), 11 | (Reference("/first/name"), Reference("/first/name"), true), 12 | (Reference(literal: "/name"), Reference(literal: "/name"), true), 13 | (Reference(literal: "/name"), Reference("/~1name"), true), 14 | (Reference(literal: "~name"), Reference("/~0name"), true), 15 | 16 | (Reference("different"), Reference("values"), false), 17 | (Reference("name/"), Reference("/name"), false), 18 | (Reference("/first/name"), Reference("/first//name"), false) 19 | ] 20 | 21 | for (lhs, rhs, expected) in tests { 22 | XCTAssertEqual(lhs == rhs, expected) 23 | } 24 | } 25 | func testFailsWithCorrectError() { 26 | let tests: [(String, ReferenceError)] = [ 27 | ("", .empty), 28 | ("/", .empty), 29 | ("//", .doubleSlash), 30 | ("/a//b", .doubleSlash), 31 | ("/a/b/", .doubleSlash), 32 | ("/~3", .invalidEscapeSequence), 33 | ("/testing~something", .invalidEscapeSequence), 34 | ("/m~~0", .invalidEscapeSequence), 35 | ("/a~", .invalidEscapeSequence) 36 | ] 37 | 38 | for (path, error) in tests { 39 | let reference = Reference(path) 40 | XCTAssertTrue(!reference.isValid()) 41 | XCTAssertEqual(reference.getError(), error) 42 | } 43 | } 44 | 45 | func testWithoutLeadingSlashes() { 46 | let tests = ["key", "kind", "name", "name/with/slashes", "name~0~1with-what-looks-like-escape-sequences"] 47 | 48 | for test in tests { 49 | let ref = Reference(test) 50 | XCTAssertTrue(ref.isValid()) 51 | XCTAssertEqual(1, ref.depth()) 52 | XCTAssertEqual(test, ref.component(0)) 53 | } 54 | } 55 | 56 | func testWithLeadingSlashes() { 57 | let tests = [ 58 | ("/key", "key"), 59 | ("/kind", "kind"), 60 | ("/name", "name"), 61 | ("/custom", "custom") 62 | ] 63 | 64 | for (ref, expected) in tests { 65 | let ref = Reference(ref) 66 | XCTAssertTrue(ref.isValid()) 67 | XCTAssertEqual(1, ref.depth()) 68 | XCTAssertEqual(expected, ref.component(0)) 69 | } 70 | } 71 | 72 | func testHandlesSubcomponents() { 73 | let tests: [(String, Int, Int, String)] = [ 74 | ("/a/b", 2, 0, "a"), 75 | ("/a/b", 2, 1, "b"), 76 | ("/a~1b/c", 2, 0, "a/b"), 77 | ("/a~1b/c", 2, 1, "c"), 78 | ("/a/10/20/30x", 4, 1, "10"), 79 | ("/a/10/20/30x", 4, 2, "20"), 80 | ("/a/10/20/30x", 4, 3, "30x") 81 | ] 82 | 83 | for (input, expectedLength, index, expectedName) in tests { 84 | let reference = Reference(input) 85 | 86 | XCTAssertEqual(expectedLength, reference.depth()) 87 | XCTAssertEqual(expectedName, reference.component(index)) 88 | } 89 | } 90 | 91 | func testCanHandleInvalidIndexRequests() { 92 | let reference = Reference("/a/b/c") 93 | 94 | XCTAssertTrue(reference.isValid()) 95 | XCTAssertNotNil(reference.component(0)) 96 | XCTAssertNotNil(reference.component(1)) 97 | XCTAssertNotNil(reference.component(2)) 98 | 99 | XCTAssertNil(reference.component(3)) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Models/FeatureFlag/FlagChange/FlagChangeObserverSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import LaunchDarkly 5 | 6 | private final class ObserverOwnerMock { 7 | var changedFlagCount = 0 8 | var changedCollectionCount = 0 9 | 10 | func handleFlagChange(changedFlag: LDChangedFlag) { 11 | changedFlagCount += 1 12 | } 13 | 14 | func handleCollectionChange(changedFlags: [LDFlagKey: LDChangedFlag]) { 15 | changedCollectionCount += 1 16 | } 17 | } 18 | 19 | final class FlagChangeObserverSpec: XCTestCase { 20 | let testChangedFlag = LDChangedFlag(key: "key1", oldValue: nil, newValue: nil) 21 | 22 | func testInit() { 23 | let ownerMock = ObserverOwnerMock() 24 | let flagChangeObserver = FlagChangeObserver(key: "key1", owner: ownerMock, flagChangeHandler: ownerMock.handleFlagChange) 25 | XCTAssert(flagChangeObserver.owner === ownerMock) 26 | XCTAssertEqual(flagChangeObserver.flagKeys, ["key1"]) 27 | XCTAssertNil(flagChangeObserver.flagCollectionChangeHandler) 28 | flagChangeObserver.flagChangeHandler?(testChangedFlag) 29 | XCTAssertEqual(ownerMock.changedFlagCount, 1) 30 | } 31 | 32 | func testInitCollection() { 33 | let ownerMock = ObserverOwnerMock() 34 | let flagChangeObserver = FlagChangeObserver(keys: ["key1"], owner: ownerMock, flagCollectionChangeHandler: ownerMock.handleCollectionChange) 35 | XCTAssert(flagChangeObserver.owner === ownerMock) 36 | XCTAssertEqual(flagChangeObserver.flagKeys, ["key1"]) 37 | XCTAssertNil(flagChangeObserver.flagChangeHandler) 38 | flagChangeObserver.flagCollectionChangeHandler?(["key1": testChangedFlag]) 39 | XCTAssertEqual(ownerMock.changedCollectionCount, 1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Networking/HTTPURLResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import LaunchDarkly 3 | 4 | extension HTTPURLResponse.StatusCodes { 5 | static let all = [ok, accepted, badRequest, unauthorized, methodNotAllowed, internalServerError, notImplemented] 6 | static let retry = LDConfig.reportRetryStatusCodes 7 | static let nonRetry = all.filter { statusCode in 8 | !LDConfig.reportRetryStatusCodes.contains(statusCode) && statusCode != ok 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import LaunchDarkly 5 | 6 | final class URLRequestSpec: XCTestCase { 7 | func testInitExtension() { 8 | var delegateArgs: (url: URL, headers: [String: String])? 9 | 10 | let url = URL(string: "https://dummy.urlRequest.com")! 11 | var config = LDConfig(mobileKey: "testkey", autoEnvAttributes: .disabled) 12 | config.connectionTimeout = 15 13 | config.headerDelegate = { url, headers in 14 | delegateArgs = (url, headers) 15 | return ["Proxy": "Other"] 16 | } 17 | let request: URLRequest = URLRequest(url: url, 18 | ldHeaders: ["Authorization": "api_key foo"], 19 | ldConfig: config) 20 | 21 | XCTAssertEqual(request.timeoutInterval, 15) 22 | XCTAssertEqual(request.url, url) 23 | XCTAssertEqual(delegateArgs?.url, url) 24 | XCTAssertEqual(delegateArgs?.headers, ["Authorization": "api_key foo"]) 25 | XCTAssertEqual(request.allHTTPHeaderFields, ["Proxy": "Other"]) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/CacheConverterSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import LaunchDarkly 5 | 6 | final class CacheConverterSpec: XCTestCase { 7 | 8 | private var serviceFactory: ClientServiceMockFactory! 9 | 10 | private static var upToDateData: Data! 11 | 12 | override class func setUp() { 13 | upToDateData = try! JSONEncoder().encode(["version": 8]) 14 | } 15 | 16 | override func setUp() { 17 | serviceFactory = ClientServiceMockFactory(config: LDConfig(mobileKey: "sdk-key", autoEnvAttributes: .disabled)) 18 | } 19 | 20 | func testNoKeysGiven() { 21 | CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: [], maxCachedContexts: 0) 22 | // We always make one call to try and clean up old v5 data. 23 | XCTAssertEqual(serviceFactory.makeKeyedValueCacheCallCount, 1) 24 | XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 0) 25 | } 26 | 27 | func testUpToDate() { 28 | let v7valueCacheMock = KeyedValueCachingMock() 29 | v7valueCacheMock.keysReturnValue = ["key1", "key2"] 30 | serviceFactory.makeFeatureFlagCacheReturnValue.keyedValueCache = v7valueCacheMock 31 | serviceFactory.makeKeyedValueCacheReturnValue = v7valueCacheMock 32 | v7valueCacheMock.dataReturnValue = CacheConverterSpec.upToDateData 33 | CacheConverter().convertCacheData(serviceFactory: serviceFactory, keysToConvert: ["key1", "key2"], maxCachedContexts: 0) 34 | XCTAssertEqual(serviceFactory.makeFeatureFlagCacheCallCount, 2) 35 | XCTAssertEqual(v7valueCacheMock.dataCallCount, 2) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporterSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import LaunchDarkly 5 | 6 | final class ApplicationInfoEnvironmentReporterSpec: XCTestCase { 7 | 8 | func testApplicationInfoReporterSpec() { 9 | var applicationInfo = ApplicationInfo() 10 | applicationInfo.applicationIdentifier("example-id") 11 | applicationInfo.applicationName("example-name") 12 | applicationInfo.applicationVersion("example-version") 13 | applicationInfo.applicationVersionName("example-version-name") 14 | 15 | let chain = EnvironmentReporterChainBase() 16 | chain.setNext(ApplicationInfoEnvironmentReporter(applicationInfo)) 17 | 18 | XCTAssertEqual(chain.applicationInfo, applicationInfo) 19 | } 20 | 21 | func testFallbackWhenIDMissing() { 22 | var applicationInfo = ApplicationInfo() 23 | applicationInfo.applicationVersion("example-version") // setting only version triggers fallback 24 | let reporter = ApplicationInfoEnvironmentReporter(applicationInfo) 25 | let output = reporter.applicationInfo 26 | XCTAssertNotNil(output.applicationId) 27 | XCTAssertNotNil(output.applicationName) 28 | XCTAssertNotNil(output.applicationVersion) 29 | XCTAssertNotNil(output.applicationVersionName) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBaseSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import LaunchDarkly 5 | 6 | final class EnvironmentReporterChainBaseSpec: XCTest { 7 | 8 | func testEmptyChainBase() { 9 | let chain = EnvironmentReporterChainBase() 10 | let appInfo = chain.applicationInfo 11 | 12 | XCTAssertEqual(appInfo.applicationId, "UNKNOWN") 13 | XCTAssertEqual(appInfo.applicationName, "UNKNOWN") 14 | XCTAssertEqual(appInfo.applicationVersion, "UNKNOWN") 15 | XCTAssertEqual(appInfo.applicationVersionName, "UNKNOWN") 16 | 17 | XCTAssertFalse(chain.isDebugBuild) 18 | 19 | XCTAssertEqual(chain.deviceModel, "UNKNOWN") 20 | XCTAssertEqual(chain.systemVersion, "UNKNOWN") 21 | XCTAssertNil(chain.vendorUUID) 22 | 23 | XCTAssertEqual(chain.manufacturer, "Apple") 24 | XCTAssertEqual(chain.osFamily, "Apple") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporterSpec.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import Foundation 3 | import XCTest 4 | 5 | @testable import LaunchDarkly 6 | 7 | final class IOSEnvironmentReporterSpec: XCTest { 8 | 9 | func testIosEnvironmentReporter() { 10 | let chain = EnvironmentReporterChainBase() 11 | chain.setNext(IOSEnvironmentReporter()) 12 | 13 | XCTAssertNotNil(chain.applicationInfo.applicationId) 14 | XCTAssertNotEqual(chain.deviceModel, "UNKNOWN") 15 | XCTAssertNotEqual(chain.systemVersion, "UNKNOWN") 16 | 17 | XCTAssertNil(chain.vendorUUID) 18 | } 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporterSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import LaunchDarkly 5 | 6 | final class SDKEnvironmentReporterSpec: XCTestCase { 7 | 8 | func testSdkEnvironmentReporter() { 9 | let reporter = SDKEnvironmentReporter() 10 | XCTAssertEqual(reporter.applicationInfo.applicationId, ReportingConsts.sdkName) 11 | XCTAssertEqual(reporter.applicationInfo.applicationName, ReportingConsts.sdkName) 12 | XCTAssertEqual(reporter.applicationInfo.applicationVersion, ReportingConsts.sdkVersion) 13 | XCTAssertEqual(reporter.applicationInfo.applicationVersionName, ReportingConsts.sdkVersion) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporterSpec.swift: -------------------------------------------------------------------------------- 1 | #if os(watchOS) 2 | import Foundation 3 | import XCTest 4 | 5 | @testable import LaunchDarkly 6 | 7 | final class WatchOSEnvironmentReporterSpec: XCTest { 8 | func testDefaultReporterBehavior() { 9 | let chain = EnvironmentReporterChainBase() 10 | chain.setNext(WatchOSEnvironmentReporter()) 11 | 12 | XCTAssert(!chain.applicationInfo.isEmpty()) 13 | XCTAssertNotEqual(chain.deviceModel, "UNKNOWN") 14 | XCTAssertNotEqual(chain.systemVersion, "UNKNOWN") 15 | XCTAssertNotEqual(chain.systemName, "UNKNOWN") 16 | 17 | XCTAssertEqual(chain.operatingSystem, .watchOS) 18 | 19 | XCTAssertNil(chain.vendorUUID) 20 | } 21 | 22 | func testBuilderDoesNotIncludeWatchInfoWithoutExplicitOptIn() { 23 | let builder = EnvironmentReporterBuilder() 24 | let reporting = builder.build() 25 | 26 | XCTAssertEqual(reporting.operatingSystem, .watchOS) 27 | } 28 | 29 | func testEnsureBuilderUsesCorrectReporter() { 30 | let builder = EnvironmentReporterBuilder() 31 | builder.enableCollectionFromPlatform() 32 | 33 | let reporting = builder.build() 34 | 35 | XCTAssertEqual(reporting.operatingSystem, .watchOS) 36 | } 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/ServiceObjects/LDTimerSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Quick 3 | import Nimble 4 | @testable import LaunchDarkly 5 | 6 | final class LDTimerSpec: QuickSpec { 7 | 8 | struct TestContext { 9 | var ldTimer: LDTimer 10 | let fireQueue: DispatchQueue = DispatchQueue(label: "LaunchDarkly.LDTimerSpec.TestContext.fireQueue") 11 | let timeInterval: TimeInterval 12 | let fireDate: Date 13 | 14 | init(timeInterval: TimeInterval = 60.0, execute: @escaping () -> Void) { 15 | self.timeInterval = timeInterval 16 | self.fireDate = Date().addingTimeInterval(timeInterval) 17 | ldTimer = LDTimer(withTimeInterval: timeInterval, fireQueue: fireQueue, execute: execute) 18 | } 19 | } 20 | 21 | override func spec() { 22 | initSpec() 23 | timerFiredSpec() 24 | cancelSpec() 25 | } 26 | 27 | private func initSpec() { 28 | describe("init") { 29 | it("creates a repeating timer") { 30 | let testContext = TestContext(execute: { }) 31 | 32 | expect(testContext.ldTimer.timer).toNot(beNil()) 33 | expect(testContext.ldTimer.isCancelled) == false 34 | expect(testContext.ldTimer.fireDate).to(beCloseTo(testContext.fireDate, within: 1.0)) // 1 second is arbitrary...just want it to be "close" 35 | 36 | testContext.ldTimer.cancel() 37 | } 38 | } 39 | } 40 | 41 | private func timerFiredSpec() { 42 | describe("timerFired") { 43 | it("calls execute on the fireQueue multiple times") { 44 | var fireCount = 0 45 | var testContext: TestContext! 46 | waitUntil { done in 47 | // timeInterval is arbitrary here. "Fast" so the test doesn't take a long time. 48 | testContext = TestContext(timeInterval: 0.01, execute: { 49 | dispatchPrecondition(condition: .onQueue(testContext.fireQueue)) 50 | if fireCount < 2 { 51 | fireCount += 1 // If the timer fires again before the test is done, that's ok. This just measures an arbitrary point in time. 52 | } else { 53 | done() 54 | } 55 | }) 56 | } 57 | 58 | expect(testContext.ldTimer.timer?.isValid) == true 59 | expect(testContext.ldTimer.isCancelled) == false 60 | expect(fireCount) == 2 61 | 62 | testContext.ldTimer.cancel() 63 | } 64 | } 65 | } 66 | 67 | private func cancelSpec() { 68 | describe("cancel") { 69 | it("cancels the timer") { 70 | let testContext = TestContext(execute: { }) 71 | testContext.ldTimer.cancel() 72 | expect(testContext.ldTimer.timer?.isValid ?? false) == false // the timer either doesn't exist or is invalid...could be either depending on timing 73 | expect(testContext.ldTimer.isCancelled) == true 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/ServiceObjects/SynchronizingErrorSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | import LDSwiftEventSource 5 | @testable import LaunchDarkly 6 | 7 | final class SynchronizingErrorSpec: XCTestCase { 8 | private let falseCases: [SynchronizingError] = 9 | [.isOffline, 10 | .streamEventWhilePolling, 11 | .data(nil), 12 | .data("data".data(using: .utf8)), 13 | .request(DummyError()), 14 | .unknownEventType("update"), 15 | .response(HTTPURLResponse(url: LDConfig.stub.streamUrl, 16 | statusCode: HTTPURLResponse.StatusCodes.internalServerError, 17 | httpVersion: "1.1", 18 | headerFields: nil)), 19 | .streamError(UnsuccessfulResponseError(responseCode: 500)) 20 | ] 21 | private let trueCases: [SynchronizingError] = 22 | [.response(HTTPURLResponse(url: LDConfig.stub.streamUrl, 23 | statusCode: HTTPURLResponse.StatusCodes.unauthorized, 24 | httpVersion: "1.1", 25 | headerFields: nil)), 26 | .streamError(UnsuccessfulResponseError(responseCode: 401)) 27 | ] 28 | 29 | func testErrorShouldBeUnauthorized() { 30 | trueCases.forEach { testValue in 31 | XCTAssertTrue(testValue.isClientUnauthorized, "\(testValue) should be unauthorized") 32 | } 33 | } 34 | 35 | func testErrorShouldNotBeUnauthorized() { 36 | falseCases.forEach { testValue in 37 | XCTAssertFalse(testValue.isClientUnauthorized, "\(testValue) should not be unauthorized") 38 | } 39 | } 40 | } 41 | 42 | struct DummyError: Error { } 43 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/TestContext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | @testable import LaunchDarkly 4 | 5 | class TestContext { 6 | var config: LDConfig! 7 | var context: LDContext! 8 | var subject: LDClient! 9 | let serviceFactoryMock: ClientServiceMockFactory 10 | // mock getters based on setting up the context & subject 11 | var serviceMock: DarklyServiceMock! { 12 | subject.service as? DarklyServiceMock 13 | } 14 | var featureFlagCachingMock: FeatureFlagCachingMock! { 15 | subject.flagCache as? FeatureFlagCachingMock 16 | } 17 | var flagStoreMock: FlagMaintainingMock! { 18 | subject.flagStore as? FlagMaintainingMock 19 | } 20 | var flagSynchronizerMock: LDFlagSynchronizingMock! { 21 | subject.flagSynchronizer as? LDFlagSynchronizingMock 22 | } 23 | var eventReporterMock: EventReportingMock! { 24 | subject.eventReporter as? EventReportingMock 25 | } 26 | var changeNotifierMock: FlagChangeNotifyingMock! { 27 | subject.flagChangeNotifier as? FlagChangeNotifyingMock 28 | } 29 | var environmentReporterMock: EnvironmentReportingMock! { 30 | subject.environmentReporter as? EnvironmentReportingMock 31 | } 32 | var makeFlagSynchronizerStreamingMode: LDStreamingMode? { 33 | serviceFactoryMock.makeFlagSynchronizerReceivedParameters?.streamingMode 34 | } 35 | var makeFlagSynchronizerPollingInterval: TimeInterval? { 36 | serviceFactoryMock.makeFlagSynchronizerReceivedParameters?.pollingInterval 37 | } 38 | var makeFlagSynchronizerService: DarklyServiceProvider? { 39 | serviceFactoryMock.makeFlagSynchronizerReceivedParameters?.service 40 | } 41 | var onSyncComplete: FlagSyncCompleteClosure? { 42 | serviceFactoryMock.onFlagSyncComplete 43 | } 44 | var recordedEvent: LaunchDarkly.Event? { 45 | eventReporterMock.recordReceivedEvent 46 | } 47 | var throttlerMock: ThrottlingMock? { 48 | subject.throttler as? ThrottlingMock 49 | } 50 | 51 | private(set) var cachedFlags: [String: [String: [LDFlagKey: FeatureFlag]]] = [:] 52 | 53 | init(newConfig: LDConfig? = nil, 54 | startOnline: Bool = false, 55 | streamingMode: LDStreamingMode = .streaming, 56 | enableBackgroundUpdates: Bool = true) { 57 | config = newConfig ?? LDConfig.stub(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled, isDebugBuild: false) 58 | config.startOnline = startOnline 59 | config.streamingMode = streamingMode 60 | config.enableBackgroundUpdates = enableBackgroundUpdates 61 | config.eventFlushInterval = 300.0 // 5 min...don't want this to trigger 62 | 63 | context = LDContext.stub() 64 | 65 | serviceFactoryMock = ClientServiceMockFactory(config: config) 66 | serviceFactoryMock.makeFlagChangeNotifierReturnValue = FlagChangeNotifier(logger: OSLog(subsystem: "com.launchdarkly", category: "tests")) 67 | 68 | serviceFactoryMock.makeFeatureFlagCacheCallback = { 69 | let mobileKey = self.serviceFactoryMock.makeFeatureFlagCacheReceivedParameters!.mobileKey 70 | let mockCache = FeatureFlagCachingMock() 71 | mockCache.getCachedDataCallback = { 72 | let arguments = mockCache.getCachedDataReceivedArguments 73 | let cacheKey = arguments?.cacheKey 74 | let flags = cacheKey.map { self.cachedFlags[mobileKey]?[$0] ?? [:] } ?? [:] 75 | mockCache.getCachedDataReturnValue = (items: StoredItems(items: flags), etag: nil, lastUpdated: nil) 76 | } 77 | self.serviceFactoryMock.makeFeatureFlagCacheReturnValue = mockCache 78 | } 79 | } 80 | 81 | func withContext(_ context: LDContext?) -> TestContext { 82 | self.context = context 83 | return self 84 | } 85 | 86 | func withCached(flags: [LDFlagKey: FeatureFlag]?) -> TestContext { 87 | withCached(contextKey: context.fullyQualifiedHashedKey(), flags: flags) 88 | } 89 | 90 | func withCached(contextKey: String, flags: [LDFlagKey: FeatureFlag]?) -> TestContext { 91 | var forEnv = cachedFlags[config.mobileKey] ?? [:] 92 | forEnv[contextKey] = flags 93 | cachedFlags[config.mobileKey] = forEnv 94 | return self 95 | } 96 | 97 | func start(runMode: LDClientRunMode = .foreground, completion: (() -> Void)? = nil) { 98 | LDClient.start(serviceFactory: serviceFactoryMock, config: config, context: context) { 99 | self.subject = LDClient.get() 100 | if runMode == .background { 101 | self.subject.setRunMode(.background) 102 | } 103 | completion?() 104 | } 105 | subject = LDClient.get() 106 | } 107 | 108 | func start(runMode: LDClientRunMode = .foreground, timeOut: TimeInterval, timeOutCompletion: ((_ timedOut: Bool) -> Void)? = nil) { 109 | LDClient.start(serviceFactory: serviceFactoryMock, config: config, context: context, startWaitSeconds: timeOut) { timedOut in 110 | self.subject = LDClient.get() 111 | if runMode == .background { 112 | self.subject.setRunMode(.background) 113 | } 114 | timeOutCompletion?(timedOut) 115 | } 116 | subject = LDClient.get() 117 | } 118 | } 119 | 120 | struct DefaultFlagValues { 121 | static let bool = false 122 | static let int = 5 123 | static let double = 2.71828 124 | static let string = "default string value" 125 | static let array: LDValue = [-1, -2] 126 | static let dictionary: LDValue = ["sub-flag-x": true, "sub-flag-y": 1, "sub-flag-z": 42.42] 127 | } 128 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/TestUtil.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | 4 | @testable import LaunchDarkly 5 | 6 | func symmetricAssertEqual(_ exp1: @autoclosure () throws -> T, 7 | _ exp2: @autoclosure () throws -> T, 8 | _ message: @autoclosure () -> String = "") { 9 | XCTAssertEqual(try exp1(), try exp2(), message()) 10 | XCTAssertEqual(try exp2(), try exp1(), message()) 11 | } 12 | 13 | func symmetricAssertNotEqual(_ exp1: @autoclosure () throws -> T, 14 | _ exp2: @autoclosure () throws -> T, 15 | _ message: @autoclosure () -> String = "") { 16 | XCTAssertNotEqual(try exp1(), try exp2(), message()) 17 | XCTAssertNotEqual(try exp2(), try exp1(), message()) 18 | } 19 | 20 | func encodeToLDValue(_ value: T, userInfo: [CodingUserInfoKey: Any] = [:]) -> LDValue? { 21 | let encoder = JSONEncoder() 22 | encoder.dateEncodingStrategy = .custom { date, encoder in 23 | var container = encoder.singleValueContainer() 24 | try container.encode(date.millisSince1970) 25 | } 26 | encoder.userInfo = userInfo 27 | return try? JSONDecoder().decode(LDValue.self, from: encoder.encode(value)) 28 | } 29 | 30 | func encodesToObject(_ value: T, userInfo: [CodingUserInfoKey: Any] = [:], asserts: ([String: LDValue]) -> Void) { 31 | valueIsObject(encodeToLDValue(value, userInfo: userInfo), asserts: asserts) 32 | } 33 | 34 | func valueIsObject(_ value: LDValue?, asserts: ([String: LDValue]) -> Void) { 35 | guard case .object(let dict) = value 36 | else { 37 | XCTFail("expected value to be object got \(String(describing: value))") 38 | return 39 | } 40 | asserts(dict) 41 | } 42 | 43 | func valueIsArray(_ value: LDValue?, asserts: ([LDValue]) -> Void) { 44 | guard case .array(let arr) = value 45 | else { 46 | XCTFail("expected value to be array got \(String(describing: value))") 47 | return 48 | } 49 | asserts(arr) 50 | } 51 | -------------------------------------------------------------------------------- /LaunchDarkly/LaunchDarklyTests/UtilSpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import LaunchDarkly 5 | 6 | final class UtilSpec: XCTestCase { 7 | 8 | func testSha256base64() throws { 9 | let input = "hashThis!" 10 | let expectedOutput = "sfXg3HewbCAVNQLJzPZhnFKntWYvN0nAYyUWFGy24dQ=" 11 | let output = Util.sha256base64(input) 12 | XCTAssertEqual(output, expectedOutput) 13 | } 14 | 15 | func testSha256base64UrlEncoding() throws { 16 | let input = "OhYeah?HashThis!!!" // hash is KzDwVRpvTuf//jfMK27M4OMpIRTecNcJoaffvAEi+as= and it has a + and a / 17 | let expectedOutput = "KzDwVRpvTuf__jfMK27M4OMpIRTecNcJoaffvAEi-as=" 18 | let output = Util.sha256(input).base64UrlEncodedString 19 | XCTAssertEqual(output, expectedOutput) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEMP_TEST_OUTPUT=/tmp/contract-test-service.log 2 | 3 | build-contract-tests: 4 | cd ./ContractTests && swift build --product ContractTests 5 | 6 | start-contract-test-service: build-contract-tests 7 | cd ./ContractTests && swift run 8 | 9 | start-contract-test-service-bg: 10 | @echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)" 11 | @make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 & 12 | 13 | run-contract-tests: 14 | @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/master/downloader/run.sh \ 15 | | VERSION=v2 PARAMS="-url http://localhost:8080 -debug -status-timeout 120 -stop-service-at-end -skip-from ./ContractTests/testharness-suppressions.txt $(TEST_HARNESS_PARAMS)" sh 16 | 17 | contract-tests: start-contract-test-service-bg run-contract-tests 18 | 19 | .PHONY: build-contract-tests start-contract-test-service run-contract-tests contract-tests 20 | -------------------------------------------------------------------------------- /Mintfile: -------------------------------------------------------------------------------- 1 | realm/SwiftLint@0.43.1 2 | krzysztofzablocki/Sourcery@1.2.1 3 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "LaunchDarkly", 7 | platforms: [ 8 | .iOS(.v12), 9 | .macOS(.v10_13), 10 | .watchOS(.v4), 11 | .tvOS(.v12) 12 | ], 13 | products: [ 14 | .library( 15 | name: "LaunchDarkly", 16 | targets: ["LaunchDarkly"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", .exact("9.1.0")), 20 | .package(url: "https://github.com/Quick/Quick.git", .exact("4.0.0")), 21 | .package(url: "https://github.com/Quick/Nimble.git", .exact("9.2.1")), 22 | .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting", .exact("2.1.2")), 23 | .package(name: "LDSwiftEventSource", url: "https://github.com/LaunchDarkly/swift-eventsource.git", .exact("3.3.0")), 24 | .package(name: "DataCompression", url: "https://github.com/mw99/DataCompression", .exact("3.8.0")) 25 | ], 26 | targets: [ 27 | .target( 28 | name: "LaunchDarkly", 29 | dependencies: [ 30 | .product(name: "LDSwiftEventSource", package: "LDSwiftEventSource"), 31 | .product(name: "DataCompression", package: "DataCompression") 32 | ], 33 | path: "LaunchDarkly/LaunchDarkly", 34 | exclude: ["Support"], 35 | resources: [ 36 | .process("PrivacyInfo.xcprivacy") 37 | ]), 38 | .testTarget( 39 | name: "LaunchDarklyTests", 40 | dependencies: [ 41 | "LaunchDarkly", 42 | .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"), 43 | .product(name: "Quick", package: "Quick"), 44 | .product(name: "CwlPreconditionTesting", package: "CwlPreconditionTesting"), 45 | .product(name: "Nimble", package: "Nimble") 46 | ], 47 | path: "LaunchDarkly", 48 | exclude: ["LaunchDarklyTests/Info.plist", "LaunchDarklyTests/.swiftlint.yml"], 49 | sources: ["GeneratedCode", "LaunchDarklyTests"]), 50 | ], 51 | swiftLanguageVersions: [.v5]) 52 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting and Fixing Security Issues 2 | 3 | Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty. 4 | 5 | Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors. 6 | -------------------------------------------------------------------------------- /SourceryTemplates/mocks.stencil: -------------------------------------------------------------------------------- 1 | {% for argument in argument.imports %} 2 | import {{ argument }} 3 | {% endfor %} 4 | {% if argument.app %}@testable import {{ argument.app }}{% endif %} 5 | 6 | // swiftlint:disable large_tuple 7 | {% for type in types.protocols %} 8 | {% if type.annotations.autoMockable %} 9 | 10 | // MARK: - {{ type.name}}Mock 11 | final class {{ type.name }}Mock: {{ type.name }} { 12 | {% for variable in type.allVariables|!annotated:"noMock" %} 13 | 14 | var {{ variable.name }}SetCount = 0 15 | var set{{ variable.name|upperFirstLetter }}Callback: (() throws -> Void)? 16 | var {{ variable.name }}: {{ variable.typeName }}{% if variable|annotated:"defaultMockValue" %} = {{ variable.annotations.defaultMockValue }}{% elif variable.isOptional %} = nil{% elif variable.isArray %} = []{% elif variable.isDictionary %} = [:]{% else %} // You must annotate mocked variables that are not optional, arrays, or dictionaries, using a comment: //sourcery: defaultMockValue = {% endif %} { 17 | didSet { 18 | {{ variable.name }}SetCount += 1 19 | try! set{{ variable.name|upperFirstLetter }}Callback?() 20 | } 21 | } 22 | {% endfor %} 23 | {% for method in type.allMethods|!annotated:"noMock" %} 24 | 25 | var {{ method.callName }}CallCount = 0 26 | var {{ method.callName }}Callback: (() throws -> Void)? 27 | {% if method.throws %} var {{ method.callName }}ShouldThrow: Error?{% endif %} 28 | {% if method.parameters.count == 1 %} var {{ method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ param.typeName.unwrappedTypeName }}?{% endfor %} 29 | {% else %}{% if not method.parameters.count == 0 %} var {{ method.callName }}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% if param.typeAttributes.escaping %}{{ param.unwrappedTypeName }}{% else %}{{ param.typeName }}{% endif %}{% if not forloop.last %}, {% endif %}{% endfor %})?{% endif %} 30 | {% endif %} 31 | {% if not method.returnTypeName.isVoid %} var {{ method.callName }}ReturnValue: {{ method.returnTypeName }}{% if method.annotations.DefaultReturnValue %} = {{ method.annotations.DefaultReturnValue }}{% else %}{% if not method.isOptionalReturnType %}!{% endif %}{% endif %}{% endif %} 32 | func {{ method.shortName }}({% for param in method.parameters %}{% if param.argumentLabel == nil %}_{% else %}{{ param.argumentLabel }}{% endif %}{% if not param.argumentLabel == param.name %} {{ param.name }}{% endif %}: {{ param.typeName }}{% if not forloop.last %}, {% endif %}{% endfor %}){% if method.throws %} throws{% endif %}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { 33 | {{ method.callName }}CallCount += 1 34 | {%if method.parameters.count == 1 %} {{ method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %}{% else %}{% if not method.parameters.count == 0 %} {{ method.callName }}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}){% endif %}{% if not method.returnTypeName.isVoid %}{% endif %}{% endif %} 35 | {% if method.throws %} if let {{ method.callName }}ShouldThrow = {{ method.callName }}ShouldThrow { throw {{ method.callName }}ShouldThrow }{% endif %} 36 | try! {{ method.callName }}Callback?() 37 | {% if not method.returnTypeName.isVoid %} 38 | return {{ method.callName }}ReturnValue{% endif %} 39 | } 40 | {% endfor %} 41 | } 42 | {% endif %} 43 | {% endfor %} 44 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "release-type": "simple", 5 | "bump-minor-pre-major": true, 6 | "versioning": "default", 7 | "include-v-in-tag": false, 8 | "include-component-in-tag": false, 9 | "extra-files": [ 10 | "LaunchDarkly.podspec", 11 | "LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ReportingConsts.swift", 12 | "README.md" 13 | ] 14 | } 15 | } 16 | } 17 | --------------------------------------------------------------------------------