From 4322bdaf269fdc5aaaa1285b4399d5ad6e7b16ff Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sun, 5 May 2024 14:24:25 -0500 Subject: [PATCH 01/27] starting swift packages --- Package.swift | 23 +++++++++++++++++++ Sources/FeatherQuill/FeatherQuill.swift | 2 ++ .../FeatherQuillTests/FeatherQuillTests.swift | 12 ++++++++++ 3 files changed, 37 insertions(+) create mode 100644 Package.swift create mode 100644 Sources/FeatherQuill/FeatherQuill.swift create mode 100644 Tests/FeatherQuillTests/FeatherQuillTests.swift diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..4539caa --- /dev/null +++ b/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "FeatherQuill", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "FeatherQuill", + targets: ["FeatherQuill"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "FeatherQuill"), + .testTarget( + name: "FeatherQuillTests", + dependencies: ["FeatherQuill"]), + ] +) diff --git a/Sources/FeatherQuill/FeatherQuill.swift b/Sources/FeatherQuill/FeatherQuill.swift new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/Sources/FeatherQuill/FeatherQuill.swift @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/Tests/FeatherQuillTests/FeatherQuillTests.swift b/Tests/FeatherQuillTests/FeatherQuillTests.swift new file mode 100644 index 0000000..b89d579 --- /dev/null +++ b/Tests/FeatherQuillTests/FeatherQuillTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import FeatherQuill + +final class FeatherQuillTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} From 2c0f8feb680122b7ef0e6afa6e7b8efa180d1580 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sun, 5 May 2024 15:03:06 -0500 Subject: [PATCH 02/27] updating packages --- .github/workflows/FeatherQuill.yml | 242 +++++++++++++++++ .gitignore | 47 +++- .hound.yml | 2 + .periphery.yml | 1 + .spi.yml | 4 + .swift-version | 1 + .swiftformat | 7 + .swiftlint.yml | 118 +++++++++ Mintfile | 2 + Package.swift | 2 +- Scripts/gh-md-toc | 411 +++++++++++++++++++++++++++++ Scripts/lint.sh | 44 +++ codecov.yml | 2 + project.yml | 13 + 14 files changed, 893 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/FeatherQuill.yml create mode 100644 .hound.yml create mode 100644 .periphery.yml create mode 100644 .spi.yml create mode 100644 .swift-version create mode 100644 .swiftformat create mode 100644 .swiftlint.yml create mode 100644 Mintfile create mode 100755 Scripts/gh-md-toc create mode 100755 Scripts/lint.sh create mode 100644 codecov.yml create mode 100644 project.yml diff --git a/.github/workflows/FeatherQuill.yml b/.github/workflows/FeatherQuill.yml new file mode 100644 index 0000000..23d57ab --- /dev/null +++ b/.github/workflows/FeatherQuill.yml @@ -0,0 +1,242 @@ +name: macOS +on: + push: + branches-ignore: + - '*WIP' +env: + PACKAGE_NAME: FeatherQuill +jobs: + build-ubuntu: + name: Build on Ubuntu + env: + PACKAGE_NAME: Options + SWIFT_VER: ${{ matrix.swift-version }} + runs-on: ${{ matrix.runs-on }} + if: "!contains(github.event.head_commit.message, 'ci skip')" + strategy: + matrix: + runs-on: [ubuntu-20.04, ubuntu-22.04] + swift-version: ["5.7.1", "5.8.1", "5.9", "5.9.2", "5.10"] + steps: + - uses: actions/checkout@v4 + - name: Set Ubuntu Release DOT + run: echo "RELEASE_DOT=$(lsb_release -sr)" >> $GITHUB_ENV + - name: Set Ubuntu Release NUM + run: echo "RELEASE_NUM=${RELEASE_DOT//[-._]/}" >> $GITHUB_ENV + - name: Set Ubuntu Codename + run: echo "RELEASE_NAME=$(lsb_release -sc)" >> $GITHUB_ENV + - name: Cache swift package modules + id: cache-spm-linux + uses: actions/cache@v4 + env: + cache-name: cache-spm + with: + path: .build + key: ${{ runner.os }}-${{ env.RELEASE_DOT }}-${{ env.cache-name }}-${{ matrix.swift-version }}-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ runner.os }}-${{ env.RELEASE_DOT }}-${{ env.cache-name }}-${{ matrix.swift-version }}- + ${{ runner.os }}-${{ env.RELEASE_DOT }}-${{ env.cache-name }}- + - name: Cache swift + id: cache-swift-linux + uses: actions/cache@v4 + env: + cache-name: cache-swift + with: + path: swift-${{ env.SWIFT_VER }}-RELEASE-ubuntu${{ env.RELEASE_DOT }} + key: ${{ runner.os }}-${{ env.cache-name }}-${{ matrix.swift-version }}-${{ env.RELEASE_DOT }} + restore-keys: | + ${{ runner.os }}-${{ env.cache-name }}-${{ matrix.swift-version }}- + - name: Download Swift + if: steps.cache-swift-linux.outputs.cache-hit != 'true' + run: curl -O https://download.swift.org/swift-${SWIFT_VER}-release/ubuntu${RELEASE_NUM}/swift-${SWIFT_VER}-RELEASE/swift-${SWIFT_VER}-RELEASE-ubuntu${RELEASE_DOT}.tar.gz + - name: Extract Swift + if: steps.cache-swift-linux.outputs.cache-hit != 'true' + run: tar xzf swift-${SWIFT_VER}-RELEASE-ubuntu${RELEASE_DOT}.tar.gz + - name: Add Path + run: echo "$GITHUB_WORKSPACE/swift-${SWIFT_VER}-RELEASE-ubuntu${RELEASE_DOT}/usr/bin" >> $GITHUB_PATH + - name: Test + run: swift test --enable-code-coverage + - uses: sersoft-gmbh/swift-coverage-action@v4 + id: coverage-files + with: + fail-on-empty-output: true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + flags: swift-${{ matrix.swift-version }},ubuntu-${{ matrix.RELEASE_DOT }} + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }} + build-macos: + name: Build on macOS + runs-on: ${{ matrix.os }} + if: "!contains(github.event.head_commit.message, 'ci skip')" + env: + PACKAGE_NAME: Options + strategy: + matrix: + include: + - xcode: "/Applications/Xcode_14.1.app" + os: macos-12 + iOSVersion: "16.1" + watchOSVersion: "9.0" + watchName: "Apple Watch Series 5 - 40mm" + iPhoneName: "iPhone 12 mini" + - xcode: "/Applications/Xcode_14.2.app" + os: macos-12 + iOSVersion: "16.2" + watchOSVersion: "9.1" + watchName: "Apple Watch Ultra (49mm)" + iPhoneName: "iPhone 14" + - xcode: "/Applications/Xcode_15.0.1.app" + os: macos-13 + iOSVersion: "17.0.1" + watchOSVersion: "10.0" + watchName: "Apple Watch Series 9 (41mm)" + iPhoneName: "iPhone 15" + - xcode: "/Applications/Xcode_15.1.app" + os: macos-13 + iOSVersion: "17.2" + watchOSVersion: "10.2" + watchName: "Apple Watch Series 9 (45mm)" + iPhoneName: "iPhone 15 Plus" + - xcode: "/Applications/Xcode_15.2.app" + os: macos-14 + iOSVersion: "17.2" + watchOSVersion: "10.2" + watchName: "Apple Watch Ultra (49mm)" + iPhoneName: "iPhone 15 Pro" + - xcode: "/Applications/Xcode_15.3.app" + os: macos-14 + iOSVersion: "17.4" + watchOSVersion: "10.4" + watchName: "Apple Watch Ultra 2 (49mm)" + iPhoneName: "iPhone 15 Pro Max" + steps: + - uses: actions/checkout@v4 + - name: Cache swift package modules + id: cache-spm-macos + uses: actions/cache@v4 + env: + cache-name: cache-spm + with: + path: .build + key: ${{ matrix.os }}-build-${{ env.cache-name }}-${{ matrix.xcode }}-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ matrix.os }}-build-${{ env.cache-name }}-${{ matrix.xcode }}- + - name: Cache mint + if: startsWith(matrix.xcode,'/Applications/Xcode_15.3') + id: cache-mint + uses: actions/cache@v4 + env: + cache-name: cache-mint + with: + path: .mint + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Mintfile') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Set Xcode Name + run: echo "XCODE_NAME=$(basename -- ${{ matrix.xcode }} | sed 's/\.[^.]*$//' | cut -d'_' -f2)" >> $GITHUB_ENV + - name: Setup Xcode + run: sudo xcode-select -s ${{ matrix.xcode }}/Contents/Developer + - name: Install mint + if: startsWith(matrix.xcode,'/Applications/Xcode_15.3') + run: | + brew update + brew install mint + - name: Build + run: swift build + - name: Run Swift Package tests + run: swift test --enable-code-coverage + - uses: sersoft-gmbh/swift-coverage-action@v4 + id: coverage-files-spm + with: + fail-on-empty-output: true + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + files: ${{ join(fromJSON(steps.coverage-files-spm.outputs.files), ',') }} + token: ${{ secrets.CODECOV_TOKEN }} + flags: macOS,${{ env.XCODE_NAME }},${{ matrix.runs-on }} + - name: Clean up spm build directory + run: rm -rf .build + - name: Lint + run: ./scripts/lint.sh + if: startsWith(matrix.xcode,'/Applications/Xcode_15.3') + # - name: Run iOS target tests + # run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }} -sdk "iphonesimulator" -destination 'platform=iOS Simulator,name=${{ matrix.iPhoneName }},OS=${{ matrix.iOSVersion }}' -enableCodeCoverage YES build test + # - uses: sersoft-gmbh/swift-coverage-action@v4 + # id: coverage-files-iOS + # with: + # fail-on-empty-output: true + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # fail_ci_if_error: true + # verbose: true + # token: ${{ secrets.CODECOV_TOKEN }} + # files: ${{ join(fromJSON(steps.coverage-files-iOS.outputs.files), ',') }} + # flags: iOS,iOS${{ matrix.iOSVersion }},macOS,${{ env.XCODE_NAME }} + # - name: Run watchOS target tests + # run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }} -sdk "watchsimulator" -destination 'platform=watchOS Simulator,name=${{ matrix.watchName }},OS=${{ matrix.watchOSVersion }}' -enableCodeCoverage YES build test + # - uses: sersoft-gmbh/swift-coverage-action@v4 + # id: coverage-files-watchOS + # with: + # fail-on-empty-output: true + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # fail_ci_if_error: true + # verbose: true + # token: ${{ secrets.CODECOV_TOKEN }} + # files: ${{ join(fromJSON(steps.coverage-files-watchOS.outputs.files), ',') }} + # flags: watchOS,watchOS${{ matrix.watchOSVersion }},macOS,${{ env.XCODE_NAME }} + build-self: + name: Build on Self-Hosting macOS + runs-on: [self-hosted, macOS] + if: github.event.repository.owner.login == github.event.organization.login && !contains(github.event.head_commit.message, 'ci skip') + steps: + - uses: actions/checkout@v4 + - name: Cache swift package modules + id: cache-spm-macos + uses: actions/cache@v4 + env: + cache-name: cache-spm + with: + path: .build + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Cache mint + id: cache-mint + uses: actions/cache@v4 + env: + cache-name: cache-mint + with: + path: .mint + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Mintfile') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Build + run: swift build + - name: Run Swift Package tests + run: swift test --enable-code-coverage + - uses: sersoft-gmbh/swift-coverage-action@v4 + with: + fail-on-empty-output: true + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: macOS,${{ env.XCODE_NAME }} + - name: Clean up spm build directory + run: rm -rf .build + - name: Lint + run: ./scripts/lint.sh diff --git a/.gitignore b/.gitignore index 330d167..008465e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,40 @@ +# Created by https://www.toptal.com/developers/gitignore/api/swift,swiftpm,swiftpackagemanager,xcode,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=swift,swiftpm,swiftpackagemanager,xcode,macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Swift ### # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore @@ -40,11 +77,11 @@ playground.xcworkspace # Packages/ # Package.pins # Package.resolved -# *.xcodeproj +*.xcodeproj # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project -# .swiftpm +.swiftpm .build/ @@ -88,3 +125,9 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ + +.mint +Output + +# Due to support for 5.10 and below +Package.resolved \ No newline at end of file diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 0000000..6941f63 --- /dev/null +++ b/.hound.yml @@ -0,0 +1,2 @@ +swiftlint: + config_file: .swiftlint.yml diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 0000000..85b884a --- /dev/null +++ b/.periphery.yml @@ -0,0 +1 @@ +retain_public: true diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..87b23ce --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [FeatherQuill] diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..f9ce5a9 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +5.10 diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..c510d49 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,7 @@ +--indent 2 +--header "\n .*?\.swift\n SimulatorServices\n\n Created by Leo Dion.\n Copyright © {year} BrightDigit.\n\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the “Software”), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n \n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n" +--commas inline +--disable wrapMultilineStatementBraces, redundantInternal +--extensionacl on-declarations +--decimalgrouping 3,4 +--exclude .build, DerivedData, .swiftpm diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..6be46e8 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,118 @@ +opt_in_rules: + - array_init + - attributes + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - expiring_todo + - explicit_acl + - explicit_init + - explicit_top_level_acl + - fallthrough + - fatal_error_message + - file_name + - file_name_no_space + - file_types_order + - first_where + - flatmap_over_map_reduce + - force_unwrapping + - function_default_parameter_at_end + - ibinspectable_in_extension + - identical_operands + - implicit_return + - implicitly_unwrapped_optional + - indentation_width + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - nimble_operator + - nslocalizedstring_key + - nslocalizedstring_require_bundle + - number_separator + - object_literal + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strong_iboutlet + - toggle_bool + - trailing_closure + - type_contents_order + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition +analyzer_rules: + - explicit_self + - unused_declaration + - unused_import +type_body_length: + - 100 + - 200 +file_length: + - 200 + - 300 +function_body_length: + - 18 + - 40 +function_parameter_count: 8 +line_length: + - 90 + - 90 +identifier_name: + excluded: + - id +excluded: + - Tests + - DerivedData + - .build + - .swiftpm +indentation_width: + indentation_width: 2 diff --git a/Mintfile b/Mintfile new file mode 100644 index 0000000..c1dc548 --- /dev/null +++ b/Mintfile @@ -0,0 +1,2 @@ +nicklockwood/SwiftFormat@0.53.5 +realm/SwiftLint@0.54.0 \ No newline at end of file diff --git a/Package.swift b/Package.swift index 4539caa..34ae9d7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Scripts/gh-md-toc b/Scripts/gh-md-toc new file mode 100755 index 0000000..8d35839 --- /dev/null +++ b/Scripts/gh-md-toc @@ -0,0 +1,411 @@ +#!/usr/bin/env bash + +# +# Steps: +# +# 1. Download corresponding html file for some README.md: +# curl -s $1 +# +# 2. Discard rows where no substring 'user-content-' (github's markup): +# awk '/user-content-/ { ... +# +# 3.1 Get last number in each row like ' ... sitemap.js.*<\/h/)+2, RLENGTH-5) +# +# 5. Find anchor and insert it inside "(...)": +# substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) +# + +gh_toc_version="0.8.0" + +gh_user_agent="gh-md-toc v$gh_toc_version" + +# +# Download rendered into html README.md by its url. +# +# +gh_toc_load() { + local gh_url=$1 + + if type curl &>/dev/null; then + curl --user-agent "$gh_user_agent" -s "$gh_url" + elif type wget &>/dev/null; then + wget --user-agent="$gh_user_agent" -qO- "$gh_url" + else + echo "Please, install 'curl' or 'wget' and try again." + exit 1 + fi +} + +# +# Converts local md file into html by GitHub +# +# -> curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown +#

Hello world github/linguist#1 cool, and #1!

'" +gh_toc_md2html() { + local gh_file_md=$1 + local skip_header=$2 + + URL=https://api.github.com/markdown/raw + + if [ ! -z "$GH_TOC_TOKEN" ]; then + TOKEN=$GH_TOC_TOKEN + else + TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" + if [ -f "$TOKEN_FILE" ]; then + TOKEN="$(cat $TOKEN_FILE)" + fi + fi + if [ ! -z "${TOKEN}" ]; then + AUTHORIZATION="Authorization: token ${TOKEN}" + fi + + local gh_tmp_file_md=$gh_file_md + if [ "$skip_header" = "yes" ]; then + if grep -Fxq "" $gh_src; then + # cut everything before the toc + gh_tmp_file_md=$gh_file_md~~ + sed '1,//d' $gh_file_md > $gh_tmp_file_md + fi + fi + + # echo $URL 1>&2 + OUTPUT=$(curl -s \ + --user-agent "$gh_user_agent" \ + --data-binary @"$gh_tmp_file_md" \ + -H "Content-Type:text/plain" \ + -H "$AUTHORIZATION" \ + "$URL") + + rm -f $gh_file_md~~ + + if [ "$?" != "0" ]; then + echo "XXNetworkErrorXX" + fi + if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then + echo "XXRateLimitXX" + else + echo "${OUTPUT}" + fi +} + + +# +# Is passed string url +# +gh_is_url() { + case $1 in + https* | http*) + echo "yes";; + *) + echo "no";; + esac +} + +# +# TOC generator +# +gh_toc(){ + local gh_src=$1 + local gh_src_copy=$1 + local gh_ttl_docs=$2 + local need_replace=$3 + local no_backup=$4 + local no_footer=$5 + local indent=$6 + local skip_header=$7 + + if [ "$gh_src" = "" ]; then + echo "Please, enter URL or local path for a README.md" + exit 1 + fi + + + # Show "TOC" string only if working with one document + if [ "$gh_ttl_docs" = "1" ]; then + + echo "Table of Contents" + echo "=================" + echo "" + gh_src_copy="" + + fi + + if [ "$(gh_is_url "$gh_src")" == "yes" ]; then + gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy" "$indent" + if [ "${PIPESTATUS[0]}" != "0" ]; then + echo "Could not load remote document." + echo "Please check your url or network connectivity" + exit 1 + fi + if [ "$need_replace" = "yes" ]; then + echo + echo "!! '$gh_src' is not a local file" + echo "!! Can't insert the TOC into it." + echo + fi + else + local rawhtml=$(gh_toc_md2html "$gh_src" "$skip_header") + if [ "$rawhtml" == "XXNetworkErrorXX" ]; then + echo "Parsing local markdown file requires access to github API" + echo "Please make sure curl is installed and check your network connectivity" + exit 1 + fi + if [ "$rawhtml" == "XXRateLimitXX" ]; then + echo "Parsing local markdown file requires access to github API" + echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting" + TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" + echo "or place GitHub auth token here: ${TOKEN_FILE}" + exit 1 + fi + local toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy" "$indent"` + echo "$toc" + if [ "$need_replace" = "yes" ]; then + if grep -Fxq "" $gh_src && grep -Fxq "" $gh_src; then + echo "Found markers" + else + echo "You don't have or in your file...exiting" + exit 1 + fi + local ts="<\!--ts-->" + local te="<\!--te-->" + local dt=`date +'%F_%H%M%S'` + local ext=".orig.${dt}" + local toc_path="${gh_src}.toc.${dt}" + local toc_createdby="" + local toc_footer="" + # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html + # clear old TOC + sed -i${ext} "/${ts}/,/${te}/{//!d;}" "$gh_src" + # create toc file + echo "${toc}" > "${toc_path}" + if [ "${no_footer}" != "yes" ]; then + echo -e "\n${toc_createdby}\n${toc_footer}\n" >> "$toc_path" + fi + + # insert toc file + if ! sed --version > /dev/null 2>&1; then + sed -i "" "/${ts}/r ${toc_path}" "$gh_src" + else + sed -i "/${ts}/r ${toc_path}" "$gh_src" + fi + echo + if [ "${no_backup}" = "yes" ]; then + rm ${toc_path} ${gh_src}${ext} + fi + echo "!! TOC was added into: '$gh_src'" + if [ -z "${no_backup}" ]; then + echo "!! Origin version of the file: '${gh_src}${ext}'" + echo "!! TOC added into a separate file: '${toc_path}'" + fi + echo + fi + fi +} + +# +# Grabber of the TOC from rendered html +# +# $1 - a source url of document. +# It's need if TOC is generated for multiple documents. +# $2 - number of spaces used to indent. +# +gh_toc_grab() { + common_awk_script=' + modified_href = "" + split(href, chars, "") + for (i=1;i <= length(href); i++) { + c = chars[i] + res = "" + if (c == "+") { + res = " " + } else { + if (c == "%") { + res = "\\x" + } else { + res = c "" + } + } + modified_href = modified_href res + } + print sprintf("%*s", (level-1)*'"$2"', "") "* [" text "](" gh_url modified_href ")" + ' + if [ `uname -s` == "OS/390" ]; then + grepcmd="pcregrep -o" + echoargs="" + awkscript='{ + level = substr($0, length($0), 1) + text = substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5) + href = substr($0, match($0, "href=\"([^\"]+)?\"")+6, RLENGTH-7) + '"$common_awk_script"' + }' + else + grepcmd="grep -Eo" + echoargs="-e" + awkscript='{ + level = substr($0, length($0), 1) + text = substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5) + href = substr($0, match($0, "href=\"[^\"]+?\"")+6, RLENGTH-7) + '"$common_awk_script"' + }' + fi + href_regex='href=\"[^\"]+?\"' + + # if closed is on the new line, then move it on the prev line + # for example: + # was: The command foo1 + # + # became: The command foo1 + sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' | + + # find strings that corresponds to template + $grepcmd '//g' | sed 's/<\/code>//g' | + + # remove g-emoji + sed 's/]*[^<]*<\/g-emoji> //g' | + + # now all rows are like: + # ... /dev/null`; then + echo `$tool --version | head -n 1` + else + echo "not installed" + fi + done +} + +show_help() { + local app_name=$(basename "$0") + echo "GitHub TOC generator ($app_name): $gh_toc_version" + echo "" + echo "Usage:" + echo " $app_name [options] src [src] Create TOC for a README file (url or local path)" + echo " $app_name - Create TOC for markdown from STDIN" + echo " $app_name --help Show help" + echo " $app_name --version Show version" + echo "" + echo "Options:" + echo " --indent Set indent size. Default: 3." + echo " --insert Insert new TOC into original file. For local files only. Default: false." + echo " See https://github.com/ekalinin/github-markdown-toc/issues/41 for details." + echo " --no-backup Remove backup file. Set --insert as well. Default: false." + echo " --hide-footer Do not write date & author of the last TOC update. Set --insert as well. Default: false." + echo " --skip-header Hide entry of the topmost headlines. Default: false." + echo " See https://github.com/ekalinin/github-markdown-toc/issues/125 for details." + echo "" +} + +# +# Options handlers +# +gh_toc_app() { + local need_replace="no" + local indent=3 + + if [ "$1" = '--help' ] || [ $# -eq 0 ] ; then + show_help + return + fi + + if [ "$1" = '--version' ]; then + show_version + return + fi + + if [ "$1" = '--indent' ]; then + indent="$2" + shift 2 + fi + + if [ "$1" = "-" ]; then + if [ -z "$TMPDIR" ]; then + TMPDIR="/tmp" + elif [ -n "$TMPDIR" -a ! -d "$TMPDIR" ]; then + mkdir -p "$TMPDIR" + fi + local gh_tmp_md + if [ `uname -s` == "OS/390" ]; then + local timestamp=$(date +%m%d%Y%H%M%S) + gh_tmp_md="$TMPDIR/tmp.$timestamp" + else + gh_tmp_md=$(mktemp $TMPDIR/tmp.XXXXXX) + fi + while read input; do + echo "$input" >> "$gh_tmp_md" + done + gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" "$indent" + return + fi + + if [ "$1" = '--insert' ]; then + need_replace="yes" + shift + fi + + if [ "$1" = '--no-backup' ]; then + need_replace="yes" + no_backup="yes" + shift + fi + + if [ "$1" = '--hide-footer' ]; then + need_replace="yes" + no_footer="yes" + shift + fi + + if [ "$1" = '--skip-header' ]; then + skip_header="yes" + shift + fi + + + for md in "$@" + do + echo "" + gh_toc "$md" "$#" "$need_replace" "$no_backup" "$no_footer" "$indent" "$skip_header" + done + + echo "" + echo "" +} + +# +# Entry point +# +gh_toc_app "$@" diff --git a/Scripts/lint.sh b/Scripts/lint.sh new file mode 100755 index 0000000..31c3fa9 --- /dev/null +++ b/Scripts/lint.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +if [ -z "$SRCROOT" ]; then + SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + PACKAGE_DIR="${SCRIPT_DIR}/.." +else + PACKAGE_DIR="${SRCROOT}" +fi + +if [ -z "$GITHUB_ACTION" ]; then + MINT_CMD="/opt/homebrew/bin/mint" +else + MINT_CMD="mint" +fi + +export MINT_PATH="$PACKAGE_DIR/.mint" +MINT_ARGS="-n -m $PACKAGE_DIR/Mintfile --silent" +MINT_RUN="$MINT_CMD run $MINT_ARGS" + +pushd $PACKAGE_DIR + +$MINT_CMD bootstrap -m Mintfile + +if [ "$LINT_MODE" == "NONE" ]; then + exit +elif [ "$LINT_MODE" == "STRICT" ]; then + SWIFTFORMAT_OPTIONS="" + SWIFTLINT_OPTIONS="--strict" +else + SWIFTFORMAT_OPTIONS="" + SWIFTLINT_OPTIONS="" +fi + +pushd $PACKAGE_DIR + +if [ -z "$CI" ]; then + $MINT_RUN swiftformat . + $MINT_RUN swiftlint --fix +fi + +$MINT_RUN swiftformat --lint $SWIFTFORMAT_OPTIONS . +$MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS + +popd diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..951b97b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "Tests" diff --git a/project.yml b/project.yml new file mode 100644 index 0000000..a6b9fbf --- /dev/null +++ b/project.yml @@ -0,0 +1,13 @@ +name: FeatherQuill +settings: + LINT_MODE: ${LINT_MODE} +packages: + StealthyStash: + path: . +aggregateTargets: + Lint: + buildScripts: + - path: Scripts/lint.sh + name: Lint + basedOnDependencyAnalysis: false + schemes: {} \ No newline at end of file From 36c1111451d49daf031ff3e9c29320570b04f204 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sun, 5 May 2024 15:07:09 -0500 Subject: [PATCH 03/27] fixing workflow --- .github/workflows/FeatherQuill.yml | 68 ++++++++++++------------------ 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/.github/workflows/FeatherQuill.yml b/.github/workflows/FeatherQuill.yml index 23d57ab..2953f7c 100644 --- a/.github/workflows/FeatherQuill.yml +++ b/.github/workflows/FeatherQuill.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: runs-on: [ubuntu-20.04, ubuntu-22.04] - swift-version: ["5.7.1", "5.8.1", "5.9", "5.9.2", "5.10"] + swift-version: ["5.9", "5.9.2", "5.10"] steps: - uses: actions/checkout@v4 - name: Set Ubuntu Release DOT @@ -77,18 +77,6 @@ jobs: strategy: matrix: include: - - xcode: "/Applications/Xcode_14.1.app" - os: macos-12 - iOSVersion: "16.1" - watchOSVersion: "9.0" - watchName: "Apple Watch Series 5 - 40mm" - iPhoneName: "iPhone 12 mini" - - xcode: "/Applications/Xcode_14.2.app" - os: macos-12 - iOSVersion: "16.2" - watchOSVersion: "9.1" - watchName: "Apple Watch Ultra (49mm)" - iPhoneName: "iPhone 14" - xcode: "/Applications/Xcode_15.0.1.app" os: macos-13 iOSVersion: "17.0.1" @@ -167,33 +155,33 @@ jobs: run: ./scripts/lint.sh if: startsWith(matrix.xcode,'/Applications/Xcode_15.3') # - name: Run iOS target tests - # run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }} -sdk "iphonesimulator" -destination 'platform=iOS Simulator,name=${{ matrix.iPhoneName }},OS=${{ matrix.iOSVersion }}' -enableCodeCoverage YES build test - # - uses: sersoft-gmbh/swift-coverage-action@v4 - # id: coverage-files-iOS - # with: - # fail-on-empty-output: true - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v4 - # with: - # fail_ci_if_error: true - # verbose: true - # token: ${{ secrets.CODECOV_TOKEN }} - # files: ${{ join(fromJSON(steps.coverage-files-iOS.outputs.files), ',') }} - # flags: iOS,iOS${{ matrix.iOSVersion }},macOS,${{ env.XCODE_NAME }} - # - name: Run watchOS target tests - # run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }} -sdk "watchsimulator" -destination 'platform=watchOS Simulator,name=${{ matrix.watchName }},OS=${{ matrix.watchOSVersion }}' -enableCodeCoverage YES build test - # - uses: sersoft-gmbh/swift-coverage-action@v4 - # id: coverage-files-watchOS - # with: - # fail-on-empty-output: true - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v4 - # with: - # fail_ci_if_error: true - # verbose: true - # token: ${{ secrets.CODECOV_TOKEN }} - # files: ${{ join(fromJSON(steps.coverage-files-watchOS.outputs.files), ',') }} - # flags: watchOS,watchOS${{ matrix.watchOSVersion }},macOS,${{ env.XCODE_NAME }} + run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }} -sdk "iphonesimulator" -destination 'platform=iOS Simulator,name=${{ matrix.iPhoneName }},OS=${{ matrix.iOSVersion }}' -enableCodeCoverage YES build test + - uses: sersoft-gmbh/swift-coverage-action@v4 + id: coverage-files-iOS + with: + fail-on-empty-output: true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ join(fromJSON(steps.coverage-files-iOS.outputs.files), ',') }} + flags: iOS,iOS${{ matrix.iOSVersion }},macOS,${{ env.XCODE_NAME }} + - name: Run watchOS target tests + run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }} -sdk "watchsimulator" -destination 'platform=watchOS Simulator,name=${{ matrix.watchName }},OS=${{ matrix.watchOSVersion }}' -enableCodeCoverage YES build test + - uses: sersoft-gmbh/swift-coverage-action@v4 + id: coverage-files-watchOS + with: + fail-on-empty-output: true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ join(fromJSON(steps.coverage-files-watchOS.outputs.files), ',') }} + flags: watchOS,watchOS${{ matrix.watchOSVersion }},macOS,${{ env.XCODE_NAME }} build-self: name: Build on Self-Hosting macOS runs-on: [self-hosted, macOS] From 906ae2eb1fd388bedd96dd2630c4761d359068e0 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 6 May 2024 14:25:01 -0500 Subject: [PATCH 04/27] starting featherquill --- Package.swift | 1 + Sources/FeatherQuill/FeatherQuill.swift | 2 - Sources/FeatherQuill/Feature.swift | 29 +++++++ .../FeatherQuill/FeatureAvailability.swift | 86 +++++++++++++++++++ .../FeatureAvailabilityMetrics.swift | 37 ++++++++ Sources/FeatherQuill/FeatureFlag.swift | 40 +++++++++ Sources/FeatherQuill/FeatureFlags.swift | 6 ++ Sources/FeatherQuill/FeatureValue.swift | 41 +++++++++ Sources/FeatherQuill/UserDefaults.swift | 15 ++++ Sources/FeatherQuill/UserType.swift | 25 ++++++ 10 files changed, 280 insertions(+), 2 deletions(-) delete mode 100644 Sources/FeatherQuill/FeatherQuill.swift create mode 100644 Sources/FeatherQuill/Feature.swift create mode 100644 Sources/FeatherQuill/FeatureAvailability.swift create mode 100644 Sources/FeatherQuill/FeatureAvailabilityMetrics.swift create mode 100644 Sources/FeatherQuill/FeatureFlag.swift create mode 100644 Sources/FeatherQuill/FeatureFlags.swift create mode 100644 Sources/FeatherQuill/FeatureValue.swift create mode 100644 Sources/FeatherQuill/UserDefaults.swift create mode 100644 Sources/FeatherQuill/UserType.swift diff --git a/Package.swift b/Package.swift index 34ae9d7..fd001de 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "FeatherQuill", + platforms: [.iOS(.v17), .macCatalyst(.v17), .macOS(.v14), .tvOS(.v17), .visionOS(.v1), .watchOS(.v10)], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( diff --git a/Sources/FeatherQuill/FeatherQuill.swift b/Sources/FeatherQuill/FeatherQuill.swift deleted file mode 100644 index 08b22b8..0000000 --- a/Sources/FeatherQuill/FeatherQuill.swift +++ /dev/null @@ -1,2 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book diff --git a/Sources/FeatherQuill/Feature.swift b/Sources/FeatherQuill/Feature.swift new file mode 100644 index 0000000..39588fa --- /dev/null +++ b/Sources/FeatherQuill/Feature.swift @@ -0,0 +1,29 @@ +import Observation +import SwiftUI + +@Observable +class Feature { + internal init(value: FeatureValue, availability: FeatureAvailability) { + self.value = value + self.availability = availability + } + + let value : FeatureValue + let availability : FeatureAvailability + + var isEnabled : Binding { + return value.isEnabled + } + + var isAvailable : Bool { + return availability.value + } +} + +extension Feature { + convenience init (key : String, userType: UserType, probability: Double = 0.0, defaultValue: ValueType, shouldUpdateAvailability: Bool = true, neverRemove: Bool = true) { + let value : FeatureValue = .init(key: key, defaultValue: defaultValue) + let availablity : FeatureAvailability = .init(key: key, userType: userType, probability: probability, shouldUpdateAvailability: shouldUpdateAvailability, neverRemove: neverRemove) + self.init(value: value, availability: availablity) + } +} diff --git a/Sources/FeatherQuill/FeatureAvailability.swift b/Sources/FeatherQuill/FeatureAvailability.swift new file mode 100644 index 0000000..e09bbee --- /dev/null +++ b/Sources/FeatherQuill/FeatureAvailability.swift @@ -0,0 +1,86 @@ +import Foundation + +struct FeatureAvailability { + private init(userDefaults: UserDefaults, metricsKey: String, availabilityKey: String, shouldUpdateAvailability: Bool, metrics: FeatureAvailabilityMetrics, neverRemove: Bool) { + self.userDefaults = userDefaults + self.metricsKey = metricsKey + self.availabilityKey = availabilityKey + self.shouldUpdateAvailability = shouldUpdateAvailability + self.metrics = metrics + self.neverRemove = neverRemove + } + + + static let metricsKey = "AvailbilityMetrics" + static let isAvailableKey = "IsAvailable" + + + init ( + userDefaults: UserDefaults = .standard, + key: String, + userType: UserType, + probability: Double = 0.0, + shouldUpdateAvailability: Bool = true, + neverRemove : Bool = true + ) { + let metricsKey = [FeatureFlags.rootKey, key, Self.metricsKey].joined(separator: ".") + let availabilityKey = [FeatureFlags.rootKey, key, Self.isAvailableKey].joined(separator: ".") + // self.init(userDefaults: userDefaults, fullKey: fullKey, userType: userType, probability: probability) + self.init(userDefaults: userDefaults, metricsKey: metricsKey, availabilityKey: availabilityKey, shouldUpdateAvailability: shouldUpdateAvailability, metrics: .init(userType: userType, probability: probability), neverRemove: neverRemove) + self.initialize() + } + let userDefaults : UserDefaults + let metricsKey : String + let availabilityKey : String + let shouldUpdateAvailability: Bool + let neverRemove : Bool + let metrics : FeatureAvailabilityMetrics + + private func initializeMetrics () -> Bool { + guard shouldUpdateAvailability else { + return false + } + + if let oldMetrics : FeatureAvailabilityMetrics = self.userDefaults.metrics(forKey: self.metricsKey) { + guard metrics != oldMetrics else { + return false + } + } + + self.userDefaults.setValue(metrics, forKey: self.metricsKey) + return true + } + + private func initializeAvailability(force: Bool = false) { + let isAvailable = self.userDefaults.value(forKey: self.availabilityKey).map { _ in + self.userDefaults.bool(forKey: self.availabilityKey) + } + switch (isAvailable, force, neverRemove) { + case (true, _, true): + return + case (.some(_), false, _): + return + + case (.none, _, _): + break + case (_, true, _): + break + } + + + let value = self.metrics.calculateAvailability() + print("Updating Availability: \(value)") + self.userDefaults.setValue(value, forKey: availabilityKey) + + } + private func initialize () { + // check for availablity + let metricsHaveChanged = initializeMetrics() + initializeAvailability(force: metricsHaveChanged) + } + + var value : Bool { + assert((self.userDefaults.value(forKey: self.availabilityKey) as? Bool) != nil) + return self.userDefaults.bool(forKey: self.availabilityKey) + } +} diff --git a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift new file mode 100644 index 0000000..a413b5f --- /dev/null +++ b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift @@ -0,0 +1,37 @@ +import Foundation + +struct FeatureAvailabilityMetrics : Equatable { + internal init (value : Double) { + let rawValueDouble = floor(value) + let rawValue = Int(rawValueDouble) + let probability = ((value - rawValueDouble) * 1000).rounded() / 1000.0 + self.init(userType: .init(rawValue: rawValue), probability: probability) + } + + public init(userType: UserType, probability: Double) { + self.userType = userType + self.probability = probability + assert((probability * 1000).rounded() / 1000.0 == probability) + assert(probability <= 1.0) + } + + let userType : UserType + let probability : Double + + func calculateAvailability () -> Bool { + let value : Bool + if UserType.matches(userType) { + value = true + } else { + let randomValue : Double = .random(in: 0.0..<1.0) + print("Random Value: \(randomValue)") + value = randomValue <= self.probability + } + return value + } + + var value : Double { + Double(userType.rawValue) + probability.remainder(dividingBy: 1) + } + +} diff --git a/Sources/FeatherQuill/FeatureFlag.swift b/Sources/FeatherQuill/FeatureFlag.swift new file mode 100644 index 0000000..88153e7 --- /dev/null +++ b/Sources/FeatherQuill/FeatureFlag.swift @@ -0,0 +1,40 @@ +import SwiftUI + + +protocol FeatureFlag : EnvironmentKey where Value == Feature { + associatedtype ValueType = Bool + + static var key : String { get } + static var audience : UserType { get } + static var probability: Double { get } + static var initialValue : ValueType { get } + static var shouldUpdateAvailability : Bool { get } + static var neverRemove : Bool { get } +} + +extension FeatureFlag { + static var shouldUpdateAvailability : Bool { return true } + static var neverRemove : Bool { return true } + static var key : String { + let typeName = "\(Self.self)" + let dropCount : Int + if typeName.hasSuffix("FeatureFlag") { + dropCount = 10 + } else if typeName.hasSuffix("Feature") { + dropCount = 7 + } else { + dropCount = 0 + } + return .init(typeName.dropLast(dropCount)) + } + static var defaultValue: Feature { + .init( + key: self.key, + userType: self.audience, + probability: self.probability, + defaultValue: self.initialValue, + shouldUpdateAvailability: self.shouldUpdateAvailability, + neverRemove: self.neverRemove + ) + } +} diff --git a/Sources/FeatherQuill/FeatureFlags.swift b/Sources/FeatherQuill/FeatureFlags.swift new file mode 100644 index 0000000..4438037 --- /dev/null +++ b/Sources/FeatherQuill/FeatureFlags.swift @@ -0,0 +1,6 @@ + + +enum FeatureFlags { + static let rootKey = "FeatureFlags" + static let valueKey = "Value" +} diff --git a/Sources/FeatherQuill/FeatureValue.swift b/Sources/FeatherQuill/FeatureValue.swift new file mode 100644 index 0000000..4f73853 --- /dev/null +++ b/Sources/FeatherQuill/FeatureValue.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftUI +import Observation + +@Observable +class FeatureValue { + + internal init(userDefaults: UserDefaults = .standard, key: String, defaultValue : ValueType) { + self.userDefaults = userDefaults + self.key = key + self.defaultValue = defaultValue + let initialValue : ValueType + let fullKey = [FeatureFlags.rootKey, self.key, FeatureFlags.valueKey].joined(separator: ".") + self.fullKey = fullKey + if let currentValue = userDefaults.value(forKey: fullKey) as? ValueType { + initialValue = currentValue + } else { + print("Setting Default Value") + userDefaults.setValue(defaultValue, forKey: fullKey) + initialValue = defaultValue + } + self._isEnabled = initialValue + } + private var _isEnabled : ValueType { + didSet { + self.userDefaults.setValue(self._isEnabled, forKey: self.fullKey) + } + } + let userDefaults : UserDefaults + let key : String + let defaultValue : ValueType + let fullKey : String + var isEnabled : Binding { + .init { + return self._isEnabled + } set: { value in + self._isEnabled = value + } + } + +} diff --git a/Sources/FeatherQuill/UserDefaults.swift b/Sources/FeatherQuill/UserDefaults.swift new file mode 100644 index 0000000..29c1954 --- /dev/null +++ b/Sources/FeatherQuill/UserDefaults.swift @@ -0,0 +1,15 @@ +import Foundation + +extension UserDefaults { + func setValue(_ value: FeatureAvailabilityMetrics, forKey key: String) { + self.setValue(value.value, forKey: key) + } + + func metrics(forKey key: String) -> FeatureAvailabilityMetrics? { + guard self.object(forKey: key) != nil else { + return nil + } + let value : Double = self.double(forKey: key) + return .init(value: value) + } +} diff --git a/Sources/FeatherQuill/UserType.swift b/Sources/FeatherQuill/UserType.swift new file mode 100644 index 0000000..8a9e22f --- /dev/null +++ b/Sources/FeatherQuill/UserType.swift @@ -0,0 +1,25 @@ + +struct UserType : OptionSet { + init(rawValue: Int) { + self.rawValue = rawValue + } + + static func matches (_ value: UserType) -> Bool { + guard value.rawValue > 0 else { + return false + } + let value : Bool = .random() + print("User Matches: \(value)") + return value + } + + var rawValue: Int + + typealias RawValue = Int + + static let proSubscriber : UserType = UserType(rawValue: 1) + static let testFlightBeta : UserType = .init(rawValue: 2) + static let any : UserType = .init(rawValue: .max) + static let `default` : UserType = [.testFlightBeta , proSubscriber] + static let none : UserType = [] +} From ef3db584cebcc10afbbd6992c26e7feb47722a35 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 6 May 2024 16:39:50 -0500 Subject: [PATCH 05/27] refactoring options --- .swiftlint.yml | 2 +- Package.swift | 43 +++--- .../FeatherQuill/AvailabilityOptions.swift | 44 ++++++ Sources/FeatherQuill/Feature.swift | 95 ++++++++---- .../FeatherQuill/FeatureAvailability.swift | 139 +++++++++++------- .../FeatureAvailabilityMetrics.swift | 84 ++++++++--- Sources/FeatherQuill/FeatureFlag.swift | 101 ++++++++----- Sources/FeatherQuill/FeatureFlags.swift | 36 ++++- Sources/FeatherQuill/FeatureValue.swift | 109 +++++++++----- Sources/FeatherQuill/UserDefaults.swift | 15 -- Sources/FeatherQuill/UserType.swift | 53 ++++--- .../FeatherQuillTests/FeatherQuillTests.swift | 43 +++++- 12 files changed, 530 insertions(+), 234 deletions(-) create mode 100644 Sources/FeatherQuill/AvailabilityOptions.swift delete mode 100644 Sources/FeatherQuill/UserDefaults.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 6be46e8..7d298fb 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -42,7 +42,7 @@ opt_in_rules: - legacy_random - literal_expression_end_indentation - lower_acl_than_parent - - missing_docs + # - missing_docs - modifier_order - multiline_arguments - multiline_arguments_brackets diff --git a/Package.swift b/Package.swift index fd001de..a19980c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,24 +1,31 @@ // swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +// swiftlint:disable:next explicit_top_level_acl explicit_acl let package = Package( - name: "FeatherQuill", - platforms: [.iOS(.v17), .macCatalyst(.v17), .macOS(.v14), .tvOS(.v17), .visionOS(.v1), .watchOS(.v10)], - products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: "FeatherQuill", - targets: ["FeatherQuill"]), - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - .target( - name: "FeatherQuill"), - .testTarget( - name: "FeatherQuillTests", - dependencies: ["FeatherQuill"]), - ] + name: "FeatherQuill", + platforms: [ + .iOS(.v17), + .macCatalyst(.v17), + .macOS(.v14), + .tvOS(.v17), + .visionOS(.v1), + .watchOS(.v10) + ], + products: [ + .library( + name: "FeatherQuill", + targets: ["FeatherQuill"] + ) + ], + targets: [ + .target( + name: "FeatherQuill" + ), + .testTarget( + name: "FeatherQuillTests", + dependencies: ["FeatherQuill"] + ) + ] ) diff --git a/Sources/FeatherQuill/AvailabilityOptions.swift b/Sources/FeatherQuill/AvailabilityOptions.swift new file mode 100644 index 0000000..4c90718 --- /dev/null +++ b/Sources/FeatherQuill/AvailabilityOptions.swift @@ -0,0 +1,44 @@ +// +// AvailabilityOptions.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +public struct AvailabilityOptions: OptionSet { + public typealias RawValue = Int + + public static let `default`: AvailabilityOptions = .init() + public static let allowOverwriteAvailable: AvailabilityOptions = .init(rawValue: 1) + public static let disableUpdateAvailability: AvailabilityOptions = .init(rawValue: 2) + + public var rawValue: Int + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } +} diff --git a/Sources/FeatherQuill/Feature.swift b/Sources/FeatherQuill/Feature.swift index 39588fa..5108e5a 100644 --- a/Sources/FeatherQuill/Feature.swift +++ b/Sources/FeatherQuill/Feature.swift @@ -1,29 +1,74 @@ -import Observation -import SwiftUI +// +// Feature.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// -@Observable -class Feature { - internal init(value: FeatureValue, availability: FeatureAvailability) { - self.value = value - self.availability = availability - } - - let value : FeatureValue - let availability : FeatureAvailability - - var isEnabled : Binding { - return value.isEnabled - } - - var isAvailable : Bool { - return availability.value +#if canImport(SwiftUI) + import Observation + import SwiftUI + + @Observable + public class Feature { + private let value: FeatureValue + private let availability: FeatureAvailability + + public var isEnabled: Binding { + value.isEnabled + } + + public var isAvailable: Bool { + availability.value + } + + fileprivate init( + value: FeatureValue, + availability: FeatureAvailability + ) { + self.value = value + self.availability = availability + } } -} -extension Feature { - convenience init (key : String, userType: UserType, probability: Double = 0.0, defaultValue: ValueType, shouldUpdateAvailability: Bool = true, neverRemove: Bool = true) { - let value : FeatureValue = .init(key: key, defaultValue: defaultValue) - let availablity : FeatureAvailability = .init(key: key, userType: userType, probability: probability, shouldUpdateAvailability: shouldUpdateAvailability, neverRemove: neverRemove) - self.init(value: value, availability: availablity) + extension Feature { + public convenience init( + key: String, + defaultValue: ValueType, + userType: UserTypeValue, + probability: Double = 0.0, + options: AvailabilityOptions = [] + ) { + let value: FeatureValue = .init(key: key, defaultValue: defaultValue) + let availablity: FeatureAvailability = .init( + key: key, + userType: userType, + probability: probability, + options: options + ) + self.init(value: value, availability: availablity) + } } -} +#endif diff --git a/Sources/FeatherQuill/FeatureAvailability.swift b/Sources/FeatherQuill/FeatureAvailability.swift index e09bbee..2f6a44e 100644 --- a/Sources/FeatherQuill/FeatureAvailability.swift +++ b/Sources/FeatherQuill/FeatureAvailability.swift @@ -1,86 +1,123 @@ +// +// FeatureAvailability.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation -struct FeatureAvailability { - private init(userDefaults: UserDefaults, metricsKey: String, availabilityKey: String, shouldUpdateAvailability: Bool, metrics: FeatureAvailabilityMetrics, neverRemove: Bool) { +internal struct FeatureAvailability { + private let userDefaults: UserDefaults + private let metricsKey: String + private let availabilityKey: String + private let options: AvailabilityOptions + private let metrics: FeatureAvailabilityMetrics + + internal var value: Bool { + assert((userDefaults.object(forKey: availabilityKey) as? Bool) != nil) + return userDefaults.bool(forKey: availabilityKey) + } + + private init( + userDefaults: UserDefaults, + metricsKey: String, + availabilityKey: String, + options: AvailabilityOptions, + metrics: FeatureAvailabilityMetrics + ) { self.userDefaults = userDefaults self.metricsKey = metricsKey self.availabilityKey = availabilityKey - self.shouldUpdateAvailability = shouldUpdateAvailability + self.options = options self.metrics = metrics - self.neverRemove = neverRemove } - - - static let metricsKey = "AvailbilityMetrics" - static let isAvailableKey = "IsAvailable" - - - init ( - userDefaults: UserDefaults = .standard, + + internal init( key: String, - userType: UserType, + userType: UserTypeValue, probability: Double = 0.0, - shouldUpdateAvailability: Bool = true, - neverRemove : Bool = true + userDefaults: UserDefaults = .standard, + options: AvailabilityOptions = [] ) { - let metricsKey = [FeatureFlags.rootKey, key, Self.metricsKey].joined(separator: ".") - let availabilityKey = [FeatureFlags.rootKey, key, Self.isAvailableKey].joined(separator: ".") - // self.init(userDefaults: userDefaults, fullKey: fullKey, userType: userType, probability: probability) - self.init(userDefaults: userDefaults, metricsKey: metricsKey, availabilityKey: availabilityKey, shouldUpdateAvailability: shouldUpdateAvailability, metrics: .init(userType: userType, probability: probability), neverRemove: neverRemove) - self.initialize() + let metricsKey = [ + FeatureFlags.rootKey, key, FeatureFlags.metricsKey + ].joined(separator: ".") + let availabilityKey = [ + FeatureFlags.rootKey, key, FeatureFlags.isAvailableKey + ].joined(separator: ".") + self.init( + userDefaults: userDefaults, + metricsKey: metricsKey, + availabilityKey: availabilityKey, + options: options, + metrics: .init(userType: userType, probability: probability) + ) + initialize() } - let userDefaults : UserDefaults - let metricsKey : String - let availabilityKey : String - let shouldUpdateAvailability: Bool - let neverRemove : Bool - let metrics : FeatureAvailabilityMetrics - - private func initializeMetrics () -> Bool { - guard shouldUpdateAvailability else { + + private func initializeMetrics() -> Bool { + guard !options.contains(.disableUpdateAvailability) else { return false } - - if let oldMetrics : FeatureAvailabilityMetrics = self.userDefaults.metrics(forKey: self.metricsKey) { + + if let oldMetrics: FeatureAvailabilityMetrics = + self.userDefaults.metrics(forKey: self.metricsKey) { guard metrics != oldMetrics else { return false } } - - self.userDefaults.setValue(metrics, forKey: self.metricsKey) + + userDefaults.set(metrics, forKey: metricsKey) return true } - + private func initializeAvailability(force: Bool = false) { - let isAvailable = self.userDefaults.value(forKey: self.availabilityKey).map { _ in - self.userDefaults.bool(forKey: self.availabilityKey) + let isAvailable = userDefaults.object(forKey: availabilityKey).map { _ in + userDefaults.bool(forKey: availabilityKey) } - switch (isAvailable, force, neverRemove) { - case (true, _, true): + switch (isAvailable, force, options.contains(.allowOverwriteAvailable)) { + case (true, _, false): return case (.some(_), false, _): return - + case (.none, _, _): - break + break case (_, true, _): break } - - - let value = self.metrics.calculateAvailability() + + let value = metrics.calculateAvailability() print("Updating Availability: \(value)") - self.userDefaults.setValue(value, forKey: availabilityKey) - + userDefaults.set(value, forKey: availabilityKey) } - private func initialize () { + + private func initialize() { // check for availablity let metricsHaveChanged = initializeMetrics() initializeAvailability(force: metricsHaveChanged) } - - var value : Bool { - assert((self.userDefaults.value(forKey: self.availabilityKey) as? Bool) != nil) - return self.userDefaults.bool(forKey: self.availabilityKey) - } } diff --git a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift index a413b5f..8af1d0f 100644 --- a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift +++ b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift @@ -1,37 +1,81 @@ +// +// FeatureAvailabilityMetrics.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + import Foundation -struct FeatureAvailabilityMetrics : Equatable { - internal init (value : Double) { +internal struct FeatureAvailabilityMetrics: Equatable { + private let userType: UserTypeValue + private let probability: Double + + fileprivate var value: Double { + Double(userType.rawValue) + probability.remainder(dividingBy: 1) + } + + fileprivate init(value: Double) { let rawValueDouble = floor(value) - let rawValue = Int(rawValueDouble) - let probability = ((value - rawValueDouble) * 1000).rounded() / 1000.0 + let rawValue = UserTypeValue.RawValue(rawValueDouble) + let probability = ((value - rawValueDouble) * 1_000).rounded() / 1_000.0 self.init(userType: .init(rawValue: rawValue), probability: probability) } - - public init(userType: UserType, probability: Double) { + + internal init(userType: UserTypeValue, probability: Double) { self.userType = userType self.probability = probability - assert((probability * 1000).rounded() / 1000.0 == probability) + assert((probability * 1_000).rounded() / 1_000.0 == probability) assert(probability <= 1.0) } - - let userType : UserType - let probability : Double - - func calculateAvailability () -> Bool { - let value : Bool - if UserType.matches(userType) { + + internal func calculateAvailability() -> Bool { + let value: Bool + if UserTypeValue.includes(userType) { value = true } else { - let randomValue : Double = .random(in: 0.0..<1.0) + let randomValue: Double = .random(in: 0.0 ..< 1.0) print("Random Value: \(randomValue)") - value = randomValue <= self.probability + value = randomValue <= probability } return value } - - var value : Double { - Double(userType.rawValue) + probability.remainder(dividingBy: 1) +} + +extension UserDefaults { + internal func set(_ value: FeatureAvailabilityMetrics, forKey key: String) { + set(value.value, forKey: key) + } + + internal func metrics( + forKey key: String + ) -> FeatureAvailabilityMetrics? { + guard object(forKey: key) != nil else { + return nil + } + let value: Double = double(forKey: key) + return .init(value: value) } - } diff --git a/Sources/FeatherQuill/FeatureFlag.swift b/Sources/FeatherQuill/FeatureFlag.swift index 88153e7..3b763f5 100644 --- a/Sources/FeatherQuill/FeatureFlag.swift +++ b/Sources/FeatherQuill/FeatureFlag.swift @@ -1,40 +1,71 @@ -import SwiftUI +// +// FeatureFlag.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +#if canImport(SwiftUI) + import SwiftUI -protocol FeatureFlag : EnvironmentKey where Value == Feature { - associatedtype ValueType = Bool - - static var key : String { get } - static var audience : UserType { get } - static var probability: Double { get } - static var initialValue : ValueType { get } - static var shouldUpdateAvailability : Bool { get } - static var neverRemove : Bool { get } -} + public protocol FeatureFlag: EnvironmentKey + where Value == Feature { + associatedtype ValueType = Bool + associatedtype UserTypeValue: UserType -extension FeatureFlag { - static var shouldUpdateAvailability : Bool { return true } - static var neverRemove : Bool { return true } - static var key : String { - let typeName = "\(Self.self)" - let dropCount : Int - if typeName.hasSuffix("FeatureFlag") { - dropCount = 10 - } else if typeName.hasSuffix("Feature") { - dropCount = 7 - } else { - dropCount = 0 - } - return .init(typeName.dropLast(dropCount)) + static var key: String { get } + static var audience: UserTypeValue { get } + static var probability: Double { get } + static var initialValue: ValueType { get } + static var options: AvailabilityOptions { get } } - static var defaultValue: Feature { - .init( - key: self.key, - userType: self.audience, - probability: self.probability, - defaultValue: self.initialValue, - shouldUpdateAvailability: self.shouldUpdateAvailability, - neverRemove: self.neverRemove - ) + + extension FeatureFlag { + public static var options: AvailabilityOptions { + .default + } + + public static var key: String { + let typeName = "\(Self.self)" + let dropCount = if typeName.hasSuffix("FeatureFlag") { + 10 + } else if typeName.hasSuffix("Feature") { + 7 + } else { + 0 + } + return .init(typeName.dropLast(dropCount)) + } + + public static var defaultValue: Feature { + .init( + key: key, + defaultValue: initialValue, + userType: audience, + probability: probability + ) + } } -} +#endif diff --git a/Sources/FeatherQuill/FeatureFlags.swift b/Sources/FeatherQuill/FeatureFlags.swift index 4438037..6866481 100644 --- a/Sources/FeatherQuill/FeatureFlags.swift +++ b/Sources/FeatherQuill/FeatureFlags.swift @@ -1,6 +1,36 @@ +// +// FeatureFlags.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +internal enum FeatureFlags { + internal static let rootKey = "FeatureFlags" + internal static let valueKey = "Value" -enum FeatureFlags { - static let rootKey = "FeatureFlags" - static let valueKey = "Value" + internal static let metricsKey = "AvailbilityMetrics" + internal static let isAvailableKey = "IsAvailable" } diff --git a/Sources/FeatherQuill/FeatureValue.swift b/Sources/FeatherQuill/FeatureValue.swift index 4f73853..9a4ec5a 100644 --- a/Sources/FeatherQuill/FeatureValue.swift +++ b/Sources/FeatherQuill/FeatureValue.swift @@ -1,41 +1,78 @@ -import Foundation -import SwiftUI -import Observation +// +// FeatureValue.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// -@Observable -class FeatureValue { - - internal init(userDefaults: UserDefaults = .standard, key: String, defaultValue : ValueType) { - self.userDefaults = userDefaults - self.key = key - self.defaultValue = defaultValue - let initialValue : ValueType - let fullKey = [FeatureFlags.rootKey, self.key, FeatureFlags.valueKey].joined(separator: ".") - self.fullKey = fullKey - if let currentValue = userDefaults.value(forKey: fullKey) as? ValueType { - initialValue = currentValue - } else { - print("Setting Default Value") - userDefaults.setValue(defaultValue, forKey: fullKey) - initialValue = defaultValue +#if canImport(SwiftUI) + import Foundation + import Observation + import SwiftUI + + @Observable + public class FeatureValue { + private let userDefaults: UserDefaults + private let key: String + private let defaultValue: ValueType + private let fullKey: String + public var isEnabled: Binding { + .init { + self._isEnabled + } set: { value in + self._isEnabled = value + } } - self._isEnabled = initialValue - } - private var _isEnabled : ValueType { - didSet { - self.userDefaults.setValue(self._isEnabled, forKey: self.fullKey) + + private var _isEnabled: ValueType { + didSet { + userDefaults.setValue(_isEnabled, forKey: fullKey) + } } - } - let userDefaults : UserDefaults - let key : String - let defaultValue : ValueType - let fullKey : String - var isEnabled : Binding { - .init { - return self._isEnabled - } set: { value in - self._isEnabled = value + + internal init( + key: String, + defaultValue: ValueType, + userDefaults: UserDefaults = .standard + ) { + self.userDefaults = userDefaults + self.key = key + self.defaultValue = defaultValue + let initialValue: ValueType + let fullKey = [ + FeatureFlags.rootKey, self.key, FeatureFlags.valueKey + ].joined(separator: ".") + self.fullKey = fullKey + if let currentValue = userDefaults.value(forKey: fullKey) as? ValueType { + initialValue = currentValue + } else { + print("Setting Default Value") + userDefaults.setValue(defaultValue, forKey: fullKey) + initialValue = defaultValue + } + _isEnabled = initialValue } } - -} +#endif diff --git a/Sources/FeatherQuill/UserDefaults.swift b/Sources/FeatherQuill/UserDefaults.swift deleted file mode 100644 index 29c1954..0000000 --- a/Sources/FeatherQuill/UserDefaults.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -extension UserDefaults { - func setValue(_ value: FeatureAvailabilityMetrics, forKey key: String) { - self.setValue(value.value, forKey: key) - } - - func metrics(forKey key: String) -> FeatureAvailabilityMetrics? { - guard self.object(forKey: key) != nil else { - return nil - } - let value : Double = self.double(forKey: key) - return .init(value: value) - } -} diff --git a/Sources/FeatherQuill/UserType.swift b/Sources/FeatherQuill/UserType.swift index 8a9e22f..772a5d5 100644 --- a/Sources/FeatherQuill/UserType.swift +++ b/Sources/FeatherQuill/UserType.swift @@ -1,25 +1,32 @@ +// +// UserType.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// -struct UserType : OptionSet { - init(rawValue: Int) { - self.rawValue = rawValue - } - - static func matches (_ value: UserType) -> Bool { - guard value.rawValue > 0 else { - return false - } - let value : Bool = .random() - print("User Matches: \(value)") - return value - } - - var rawValue: Int - - typealias RawValue = Int - - static let proSubscriber : UserType = UserType(rawValue: 1) - static let testFlightBeta : UserType = .init(rawValue: 2) - static let any : UserType = .init(rawValue: .max) - static let `default` : UserType = [.testFlightBeta , proSubscriber] - static let none : UserType = [] +public protocol UserType: OptionSet where Self.RawValue: BinaryInteger { + static func includes(_ userType: Self) -> Bool } diff --git a/Tests/FeatherQuillTests/FeatherQuillTests.swift b/Tests/FeatherQuillTests/FeatherQuillTests.swift index b89d579..f462dea 100644 --- a/Tests/FeatherQuillTests/FeatherQuillTests.swift +++ b/Tests/FeatherQuillTests/FeatherQuillTests.swift @@ -1,12 +1,41 @@ -import XCTest +// +// FeatherQuillTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + @testable import FeatherQuill +import XCTest final class FeatherQuillTests: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } } From 40456f7d867952d7314ea3e738d03d7d7b4deb99 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 6 May 2024 16:58:39 -0500 Subject: [PATCH 06/27] working featherquill --- Sources/FeatherQuill/FeatureFlag.swift | 35 +++++++++++++++++++------- Sources/FeatherQuill/UserType.swift | 1 + 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Sources/FeatherQuill/FeatureFlag.swift b/Sources/FeatherQuill/FeatureFlag.swift index 3b763f5..dfd00e6 100644 --- a/Sources/FeatherQuill/FeatureFlag.swift +++ b/Sources/FeatherQuill/FeatureFlag.swift @@ -30,6 +30,23 @@ #if canImport(SwiftUI) import SwiftUI + private enum FeatureFlagSuffixes { + static let values: [String] = [ + "FeatureFlag", + "Feature" + ] + private static func dropCount(from typeName: String) -> Int? { + values.first(where: typeName.hasSuffix(_:)).map(\.count) + } + + static func key(from typeName: String) -> String { + guard let dropCount = dropCount(from: typeName) else { + return typeName + } + return .init(typeName.dropLast(dropCount)) + } + } + public protocol FeatureFlag: EnvironmentKey where Value == Feature { associatedtype ValueType = Bool @@ -43,20 +60,20 @@ } extension FeatureFlag { + public static var typeName: String { + "\(Self.self)" + } + + public static var audience: UserTypeValue { + .default + } + public static var options: AvailabilityOptions { .default } public static var key: String { - let typeName = "\(Self.self)" - let dropCount = if typeName.hasSuffix("FeatureFlag") { - 10 - } else if typeName.hasSuffix("Feature") { - 7 - } else { - 0 - } - return .init(typeName.dropLast(dropCount)) + FeatureFlagSuffixes.key(from: typeName) } public static var defaultValue: Feature { diff --git a/Sources/FeatherQuill/UserType.swift b/Sources/FeatherQuill/UserType.swift index 772a5d5..a651c9b 100644 --- a/Sources/FeatherQuill/UserType.swift +++ b/Sources/FeatherQuill/UserType.swift @@ -28,5 +28,6 @@ // public protocol UserType: OptionSet where Self.RawValue: BinaryInteger { + static var `default`: Self { get } static func includes(_ userType: Self) -> Bool } From 5b7b60f3ac93d2ca8a70a115f4b4fba7f4446ffb Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 6 May 2024 17:42:40 -0500 Subject: [PATCH 07/27] adding demo project --- .gitignore | 3 +- .../FeatureFlagsApp.xcodeproj/project.pbxproj | 376 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 +++ .../Assets.xcassets/Contents.json | 6 + Demo/FeatureFlagsApp/ContentView.swift | 30 ++ .../FeatureFlagsApp.entitlements | 10 + Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift | 17 + .../Preview Assets.xcassets/Contents.json | 6 + Demo/FeatureFlagsExample/.gitignore | 8 + Demo/FeatureFlagsExample/Package.swift | 36 ++ .../FeatureFlagsExample/AudienceType.swift | 39 ++ .../NewDesignFeature.swift | 23 ++ .../FeatureFlagsExampleTests.swift | 12 + project.yml | 5 +- 17 files changed, 652 insertions(+), 3 deletions(-) create mode 100644 Demo/FeatureFlagsApp.xcodeproj/project.pbxproj create mode 100644 Demo/FeatureFlagsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Demo/FeatureFlagsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Demo/FeatureFlagsApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Demo/FeatureFlagsApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Demo/FeatureFlagsApp/Assets.xcassets/Contents.json create mode 100644 Demo/FeatureFlagsApp/ContentView.swift create mode 100644 Demo/FeatureFlagsApp/FeatureFlagsApp.entitlements create mode 100644 Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift create mode 100644 Demo/FeatureFlagsApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Demo/FeatureFlagsExample/.gitignore create mode 100644 Demo/FeatureFlagsExample/Package.swift create mode 100644 Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift create mode 100644 Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift create mode 100644 Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift diff --git a/.gitignore b/.gitignore index 008465e..1b15b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -129,5 +129,4 @@ iOSInjectionProject/ .mint Output -# Due to support for 5.10 and below -Package.resolved \ No newline at end of file +!Demo/FeatureFlagsApp.xcodeproj \ No newline at end of file diff --git a/Demo/FeatureFlagsApp.xcodeproj/project.pbxproj b/Demo/FeatureFlagsApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..17f76e9 --- /dev/null +++ b/Demo/FeatureFlagsApp.xcodeproj/project.pbxproj @@ -0,0 +1,376 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + B387785F2BE9916B0063496A /* FeatureFlagsExample in Frameworks */ = {isa = PBXBuildFile; productRef = B387785E2BE9916B0063496A /* FeatureFlagsExample */; }; + B3E51F252BE7C6B90046D091 /* FeatureFlagsAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E51F242BE7C6B90046D091 /* FeatureFlagsAppApp.swift */; }; + B3E51F272BE7C6B90046D091 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E51F262BE7C6B90046D091 /* ContentView.swift */; }; + B3E51F292BE7C6BA0046D091 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3E51F282BE7C6BA0046D091 /* Assets.xcassets */; }; + B3E51F2C2BE7C6BA0046D091 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3E51F2B2BE7C6BA0046D091 /* Preview Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + B3E51F212BE7C6B90046D091 /* FeatureFlagsApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeatureFlagsApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B3E51F242BE7C6B90046D091 /* FeatureFlagsAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagsAppApp.swift; sourceTree = ""; }; + B3E51F262BE7C6B90046D091 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B3E51F282BE7C6BA0046D091 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B3E51F2B2BE7C6BA0046D091 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + B3E51F2D2BE7C6BA0046D091 /* FeatureFlagsApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FeatureFlagsApp.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B3E51F1E2BE7C6B90046D091 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B387785F2BE9916B0063496A /* FeatureFlagsExample in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B38778602BE991D10063496A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + B3E51F182BE7C6B90046D091 = { + isa = PBXGroup; + children = ( + B3E51F232BE7C6B90046D091 /* FeatureFlagsApp */, + B3E51F222BE7C6B90046D091 /* Products */, + B38778602BE991D10063496A /* Frameworks */, + ); + sourceTree = ""; + }; + B3E51F222BE7C6B90046D091 /* Products */ = { + isa = PBXGroup; + children = ( + B3E51F212BE7C6B90046D091 /* FeatureFlagsApp.app */, + ); + name = Products; + sourceTree = ""; + }; + B3E51F232BE7C6B90046D091 /* FeatureFlagsApp */ = { + isa = PBXGroup; + children = ( + B3E51F242BE7C6B90046D091 /* FeatureFlagsAppApp.swift */, + B3E51F262BE7C6B90046D091 /* ContentView.swift */, + B3E51F282BE7C6BA0046D091 /* Assets.xcassets */, + B3E51F2D2BE7C6BA0046D091 /* FeatureFlagsApp.entitlements */, + B3E51F2A2BE7C6BA0046D091 /* Preview Content */, + ); + path = FeatureFlagsApp; + sourceTree = ""; + }; + B3E51F2A2BE7C6BA0046D091 /* Preview Content */ = { + isa = PBXGroup; + children = ( + B3E51F2B2BE7C6BA0046D091 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B3E51F202BE7C6B90046D091 /* FeatureFlagsApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = B3E51F302BE7C6BA0046D091 /* Build configuration list for PBXNativeTarget "FeatureFlagsApp" */; + buildPhases = ( + B3E51F1D2BE7C6B90046D091 /* Sources */, + B3E51F1E2BE7C6B90046D091 /* Frameworks */, + B3E51F1F2BE7C6B90046D091 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeatureFlagsApp; + packageProductDependencies = ( + B387785E2BE9916B0063496A /* FeatureFlagsExample */, + ); + productName = FeatureFlagsApp; + productReference = B3E51F212BE7C6B90046D091 /* FeatureFlagsApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B3E51F192BE7C6B90046D091 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + B3E51F202BE7C6B90046D091 = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = B3E51F1C2BE7C6B90046D091 /* Build configuration list for PBXProject "FeatureFlagsApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B3E51F182BE7C6B90046D091; + packageReferences = ( + B387785D2BE9916B0063496A /* XCLocalSwiftPackageReference "FeatureFlagsExample" */, + ); + productRefGroup = B3E51F222BE7C6B90046D091 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B3E51F202BE7C6B90046D091 /* FeatureFlagsApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B3E51F1F2BE7C6B90046D091 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3E51F2C2BE7C6BA0046D091 /* Preview Assets.xcassets in Resources */, + B3E51F292BE7C6BA0046D091 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B3E51F1D2BE7C6B90046D091 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3E51F272BE7C6B90046D091 /* ContentView.swift in Sources */, + B3E51F252BE7C6B90046D091 /* FeatureFlagsAppApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + B3E51F2E2BE7C6BA0046D091 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.4; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + B3E51F2F2BE7C6BA0046D091 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.4; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + B3E51F312BE7C6BA0046D091 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = FeatureFlagsApp/FeatureFlagsApp.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"FeatureFlagsApp/Preview Content\""; + DEVELOPMENT_TEAM = MLT7M394S7; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.brightdigit.FeatureFlagsApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + B3E51F322BE7C6BA0046D091 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = FeatureFlagsApp/FeatureFlagsApp.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"FeatureFlagsApp/Preview Content\""; + DEVELOPMENT_TEAM = MLT7M394S7; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.brightdigit.FeatureFlagsApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B3E51F1C2BE7C6B90046D091 /* Build configuration list for PBXProject "FeatureFlagsApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B3E51F2E2BE7C6BA0046D091 /* Debug */, + B3E51F2F2BE7C6BA0046D091 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B3E51F302BE7C6BA0046D091 /* Build configuration list for PBXNativeTarget "FeatureFlagsApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B3E51F312BE7C6BA0046D091 /* Debug */, + B3E51F322BE7C6BA0046D091 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + B387785D2BE9916B0063496A /* XCLocalSwiftPackageReference "FeatureFlagsExample" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = FeatureFlagsExample; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + B387785E2BE9916B0063496A /* FeatureFlagsExample */ = { + isa = XCSwiftPackageProductDependency; + productName = FeatureFlagsExample; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = B3E51F192BE7C6B90046D091 /* Project object */; +} diff --git a/Demo/FeatureFlagsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Demo/FeatureFlagsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Demo/FeatureFlagsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Demo/FeatureFlagsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/FeatureFlagsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Demo/FeatureFlagsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Demo/FeatureFlagsApp/Assets.xcassets/AccentColor.colorset/Contents.json b/Demo/FeatureFlagsApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Demo/FeatureFlagsApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/FeatureFlagsApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Demo/FeatureFlagsApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/Demo/FeatureFlagsApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/FeatureFlagsApp/Assets.xcassets/Contents.json b/Demo/FeatureFlagsApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/FeatureFlagsApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/FeatureFlagsApp/ContentView.swift b/Demo/FeatureFlagsApp/ContentView.swift new file mode 100644 index 0000000..7ffe9c6 --- /dev/null +++ b/Demo/FeatureFlagsApp/ContentView.swift @@ -0,0 +1,30 @@ +// +// ContentView.swift +// FeatureFlagsApp +// +// Created by Leo Dion on 5/5/24. +// + +import SwiftUI +import FeatureFlagsExample + +struct ContentView: View { + @Environment(\.newDesign) var newDesign + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + + Toggle("Is Enabled", isOn: self.newDesign.isEnabled) + .disabled(!self.newDesign.isAvailable) + .opacity(self.newDesign.isAvailable ? 1.0 : 0.5) + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/Demo/FeatureFlagsApp/FeatureFlagsApp.entitlements b/Demo/FeatureFlagsApp/FeatureFlagsApp.entitlements new file mode 100644 index 0000000..18aff0c --- /dev/null +++ b/Demo/FeatureFlagsApp/FeatureFlagsApp.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift b/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift new file mode 100644 index 0000000..9a228c3 --- /dev/null +++ b/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift @@ -0,0 +1,17 @@ +// +// FeatureFlagsAppApp.swift +// FeatureFlagsApp +// +// Created by Leo Dion on 5/5/24. +// + +import SwiftUI + +@main +struct FeatureFlagsAppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Demo/FeatureFlagsApp/Preview Content/Preview Assets.xcassets/Contents.json b/Demo/FeatureFlagsApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/FeatureFlagsApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/FeatureFlagsExample/.gitignore b/Demo/FeatureFlagsExample/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Demo/FeatureFlagsExample/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Demo/FeatureFlagsExample/Package.swift b/Demo/FeatureFlagsExample/Package.swift new file mode 100644 index 0000000..0ddd102 --- /dev/null +++ b/Demo/FeatureFlagsExample/Package.swift @@ -0,0 +1,36 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "FeatureFlagsExample", + platforms: [ + .iOS(.v17), + .macCatalyst(.v17), + .macOS(.v14), + .tvOS(.v17), + .visionOS(.v1), + .watchOS(.v10) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "FeatureFlagsExample", + targets: ["FeatureFlagsExample"]), + ], + dependencies: [ + .package(name: "FeatherQuill", path: "../..") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "FeatureFlagsExample", + dependencies: [.product(name: "FeatherQuill", package: "FeatherQuill")] + ), + .testTarget( + name: "FeatureFlagsExampleTests", + dependencies: ["FeatureFlagsExample"]), + ] +) diff --git a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift new file mode 100644 index 0000000..aef2192 --- /dev/null +++ b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift @@ -0,0 +1,39 @@ +// +// AudienceType.swift +// FeatureFlagsApp +// +// Created by Leo Dion on 5/6/24. +// + +import Foundation +import FeatherQuill + +public struct AudienceType : UserType { + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static func includes (_ value: AudienceType) -> Bool { + guard value.rawValue > 0 else { + return false + } + let value : Bool = .random() + print("User Matches: \(value)") + return value + } + + public var rawValue: Int + + public typealias RawValue = Int + + public static let proSubscriber : AudienceType = AudienceType(rawValue: 1) + public static let testFlightBeta : AudienceType = .init(rawValue: 2) + public static let any : AudienceType = .init(rawValue: .max) + public static let `default` : AudienceType = [.testFlightBeta , proSubscriber] + public static let none : AudienceType = [] +} + + + + diff --git a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift new file mode 100644 index 0000000..f4ad64f --- /dev/null +++ b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift @@ -0,0 +1,23 @@ +// +// NewDesignFeature.swift +// FeatureFlagsApp +// +// Created by Leo Dion on 5/6/24. +// + +import Foundation +import FeatherQuill +import SwiftUI + +struct NewDesignFeature : FeatureFlag { + typealias UserTypeValue = AudienceType + + static let probability: Double = 0.5 + static let initialValue: Bool = false +} + +extension EnvironmentValues { + public var newDesign: Feature { + get { self[NewDesignFeature.self] } + } +} diff --git a/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift b/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift new file mode 100644 index 0000000..9f4fad0 --- /dev/null +++ b/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import FeatureFlagsExample + +final class FeatureFlagsExampleTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/project.yml b/project.yml index a6b9fbf..e00d87e 100644 --- a/project.yml +++ b/project.yml @@ -2,8 +2,11 @@ name: FeatherQuill settings: LINT_MODE: ${LINT_MODE} packages: - StealthyStash: + FeatherQuill: path: . +projectReferences: + Demo: + path: ./Demo/FeatureFlagsApp.xcodeproj aggregateTargets: Lint: buildScripts: From 80ffbba00f8a10dcc4ac6801072f71eb46b88c2d Mon Sep 17 00:00:00 2001 From: leogdion Date: Mon, 6 May 2024 17:44:09 -0500 Subject: [PATCH 08/27] Update FeatherQuill.yml --- .github/workflows/FeatherQuill.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/FeatherQuill.yml b/.github/workflows/FeatherQuill.yml index 2953f7c..ac315ce 100644 --- a/.github/workflows/FeatherQuill.yml +++ b/.github/workflows/FeatherQuill.yml @@ -154,7 +154,7 @@ jobs: - name: Lint run: ./scripts/lint.sh if: startsWith(matrix.xcode,'/Applications/Xcode_15.3') - # - name: Run iOS target tests + - name: Run iOS target tests run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }} -sdk "iphonesimulator" -destination 'platform=iOS Simulator,name=${{ matrix.iPhoneName }},OS=${{ matrix.iOSVersion }}' -enableCodeCoverage YES build test - uses: sersoft-gmbh/swift-coverage-action@v4 id: coverage-files-iOS From 6e6304fd516428196e530f9a045bc6315940a178 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 6 May 2024 17:49:48 -0500 Subject: [PATCH 09/27] fix env variable --- .github/workflows/FeatherQuill.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/FeatherQuill.yml b/.github/workflows/FeatherQuill.yml index ac315ce..db3dc96 100644 --- a/.github/workflows/FeatherQuill.yml +++ b/.github/workflows/FeatherQuill.yml @@ -9,7 +9,6 @@ jobs: build-ubuntu: name: Build on Ubuntu env: - PACKAGE_NAME: Options SWIFT_VER: ${{ matrix.swift-version }} runs-on: ${{ matrix.runs-on }} if: "!contains(github.event.head_commit.message, 'ci skip')" @@ -72,8 +71,6 @@ jobs: name: Build on macOS runs-on: ${{ matrix.os }} if: "!contains(github.event.head_commit.message, 'ci skip')" - env: - PACKAGE_NAME: Options strategy: matrix: include: From 5a3379a0999f0ca20a36e35951c09ca9bb320961 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 6 May 2024 20:17:32 -0500 Subject: [PATCH 10/27] setting up tests --- .swiftlint.yml | 1 + Demo/FeatureFlagsApp/ContentView.swift | 36 ++++++-- Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift | 34 +++++-- Demo/FeatureFlagsExample/Package.swift | 60 +++++++------ .../FeatureFlagsExample/AudienceType.swift | 49 ++++++---- .../NewDesignFeature.swift | 38 ++++++-- .../FeatureFlagsExampleTests.swift | 43 +++++++-- Sources/FeatherQuill/Feature.swift | 8 +- .../FeatureAvailabilityMetrics.swift | 2 +- Sources/FeatherQuill/FeatureFlag.swift | 2 +- Sources/FeatherQuill/FeatureValue.swift | 3 +- Tests/FeatherQuillTests/FeatureTests.swift | 90 +++++++++++++++++++ 12 files changed, 284 insertions(+), 82 deletions(-) create mode 100644 Tests/FeatherQuillTests/FeatureTests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 7d298fb..ca1014e 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -114,5 +114,6 @@ excluded: - DerivedData - .build - .swiftpm + - Demo indentation_width: indentation_width: 2 diff --git a/Demo/FeatureFlagsApp/ContentView.swift b/Demo/FeatureFlagsApp/ContentView.swift index 7ffe9c6..df9c915 100644 --- a/Demo/FeatureFlagsApp/ContentView.swift +++ b/Demo/FeatureFlagsApp/ContentView.swift @@ -1,12 +1,34 @@ // // ContentView.swift -// FeatureFlagsApp +// SimulatorServices // -// Created by Leo Dion on 5/5/24. +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. // -import SwiftUI import FeatureFlagsExample +import SwiftUI struct ContentView: View { @Environment(\.newDesign) var newDesign @@ -16,10 +38,10 @@ struct ContentView: View { .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") - - Toggle("Is Enabled", isOn: self.newDesign.isEnabled) - .disabled(!self.newDesign.isAvailable) - .opacity(self.newDesign.isAvailable ? 1.0 : 0.5) + + Toggle("Is Enabled", isOn: newDesign.value) + .disabled(!newDesign.isAvailable) + .opacity(newDesign.isAvailable ? 1.0 : 0.5) } .padding() } diff --git a/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift b/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift index 9a228c3..71a0a7b 100644 --- a/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift +++ b/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift @@ -1,17 +1,39 @@ // // FeatureFlagsAppApp.swift -// FeatureFlagsApp +// SimulatorServices // -// Created by Leo Dion on 5/5/24. +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. // import SwiftUI @main struct FeatureFlagsAppApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } + var body: some Scene { + WindowGroup { + ContentView() } + } } diff --git a/Demo/FeatureFlagsExample/Package.swift b/Demo/FeatureFlagsExample/Package.swift index 0ddd102..dea7435 100644 --- a/Demo/FeatureFlagsExample/Package.swift +++ b/Demo/FeatureFlagsExample/Package.swift @@ -4,33 +4,35 @@ import PackageDescription let package = Package( - name: "FeatureFlagsExample", - platforms: [ - .iOS(.v17), - .macCatalyst(.v17), - .macOS(.v14), - .tvOS(.v17), - .visionOS(.v1), - .watchOS(.v10) - ], - products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: "FeatureFlagsExample", - targets: ["FeatureFlagsExample"]), - ], - dependencies: [ - .package(name: "FeatherQuill", path: "../..") - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - .target( - name: "FeatureFlagsExample", - dependencies: [.product(name: "FeatherQuill", package: "FeatherQuill")] - ), - .testTarget( - name: "FeatureFlagsExampleTests", - dependencies: ["FeatureFlagsExample"]), - ] + name: "FeatureFlagsExample", + platforms: [ + .iOS(.v17), + .macCatalyst(.v17), + .macOS(.v14), + .tvOS(.v17), + .visionOS(.v1), + .watchOS(.v10) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "FeatureFlagsExample", + targets: ["FeatureFlagsExample"] + ) + ], + dependencies: [ + .package(name: "FeatherQuill", path: "../..") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "FeatureFlagsExample", + dependencies: [.product(name: "FeatherQuill", package: "FeatherQuill")] + ), + .testTarget( + name: "FeatureFlagsExampleTests", + dependencies: ["FeatureFlagsExample"] + ) + ] ) diff --git a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift index aef2192..5574693 100644 --- a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift +++ b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift @@ -1,24 +1,45 @@ // // AudienceType.swift -// FeatureFlagsApp +// SimulatorServices // -// Created by Leo Dion on 5/6/24. +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. // -import Foundation import FeatherQuill +import Foundation -public struct AudienceType : UserType { - +public struct AudienceType: UserType { public init(rawValue: Int) { self.rawValue = rawValue } - public static func includes (_ value: AudienceType) -> Bool { + public static func includes(_ value: AudienceType) -> Bool { guard value.rawValue > 0 else { return false } - let value : Bool = .random() + let value: Bool = .random() print("User Matches: \(value)") return value } @@ -27,13 +48,9 @@ public struct AudienceType : UserType { public typealias RawValue = Int - public static let proSubscriber : AudienceType = AudienceType(rawValue: 1) - public static let testFlightBeta : AudienceType = .init(rawValue: 2) - public static let any : AudienceType = .init(rawValue: .max) - public static let `default` : AudienceType = [.testFlightBeta , proSubscriber] - public static let none : AudienceType = [] + public static let proSubscriber: AudienceType = .init(rawValue: 1) + public static let testFlightBeta: AudienceType = .init(rawValue: 2) + public static let any: AudienceType = .init(rawValue: .max) + public static let `default`: AudienceType = [.testFlightBeta, proSubscriber] + public static let none: AudienceType = [] } - - - - diff --git a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift index f4ad64f..93d689d 100644 --- a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift +++ b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift @@ -1,23 +1,43 @@ // // NewDesignFeature.swift -// FeatureFlagsApp +// SimulatorServices // -// Created by Leo Dion on 5/6/24. +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. // -import Foundation import FeatherQuill +import Foundation import SwiftUI -struct NewDesignFeature : FeatureFlag { +struct NewDesignFeature: FeatureFlag { typealias UserTypeValue = AudienceType - + static let probability: Double = 0.5 - static let initialValue: Bool = false + static let initialValue = false } extension EnvironmentValues { - public var newDesign: Feature { - get { self[NewDesignFeature.self] } - } + public var newDesign: Feature { self[NewDesignFeature.self] } } diff --git a/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift b/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift index 9f4fad0..f4a55e8 100644 --- a/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift +++ b/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift @@ -1,12 +1,41 @@ -import XCTest +// +// FeatureFlagsExampleTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + @testable import FeatureFlagsExample +import XCTest final class FeatureFlagsExampleTests: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } } diff --git a/Sources/FeatherQuill/Feature.swift b/Sources/FeatherQuill/Feature.swift index 5108e5a..d220e31 100644 --- a/Sources/FeatherQuill/Feature.swift +++ b/Sources/FeatherQuill/Feature.swift @@ -33,11 +33,11 @@ @Observable public class Feature { - private let value: FeatureValue + private let featureValue: FeatureValue private let availability: FeatureAvailability - public var isEnabled: Binding { - value.isEnabled + public var value: Binding { + featureValue.value } public var isAvailable: Bool { @@ -48,7 +48,7 @@ value: FeatureValue, availability: FeatureAvailability ) { - self.value = value + featureValue = value self.availability = availability } } diff --git a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift index 8af1d0f..efe07df 100644 --- a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift +++ b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift @@ -47,7 +47,7 @@ internal struct FeatureAvailabilityMetrics: Equatable { internal init(userType: UserTypeValue, probability: Double) { self.userType = userType self.probability = probability - assert((probability * 1_000).rounded() / 1_000.0 == probability) + // assert((probability * 1_000).rounded() / 1_000.0 == probability) assert(probability <= 1.0) } diff --git a/Sources/FeatherQuill/FeatureFlag.swift b/Sources/FeatherQuill/FeatureFlag.swift index dfd00e6..2d6de89 100644 --- a/Sources/FeatherQuill/FeatureFlag.swift +++ b/Sources/FeatherQuill/FeatureFlag.swift @@ -60,7 +60,7 @@ } extension FeatureFlag { - public static var typeName: String { + private static var typeName: String { "\(Self.self)" } diff --git a/Sources/FeatherQuill/FeatureValue.swift b/Sources/FeatherQuill/FeatureValue.swift index 9a4ec5a..7703701 100644 --- a/Sources/FeatherQuill/FeatureValue.swift +++ b/Sources/FeatherQuill/FeatureValue.swift @@ -38,7 +38,7 @@ private let key: String private let defaultValue: ValueType private let fullKey: String - public var isEnabled: Binding { + public var value: Binding { .init { self._isEnabled } set: { value in @@ -68,7 +68,6 @@ if let currentValue = userDefaults.value(forKey: fullKey) as? ValueType { initialValue = currentValue } else { - print("Setting Default Value") userDefaults.setValue(defaultValue, forKey: fullKey) initialValue = defaultValue } diff --git a/Tests/FeatherQuillTests/FeatureTests.swift b/Tests/FeatherQuillTests/FeatureTests.swift new file mode 100644 index 0000000..08566af --- /dev/null +++ b/Tests/FeatherQuillTests/FeatureTests.swift @@ -0,0 +1,90 @@ +// +// FeatureTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +@testable import FeatherQuill +import XCTest + +public struct AudienceType: UserType { + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static func includes(_ value: AudienceType) -> Bool { + guard value.rawValue > 0 else { + return false + } + let value: Bool = .random() + print("User Matches: \(value)") + return value + } + + public var rawValue: Int + + public typealias RawValue = Int + + public static let proSubscriber: AudienceType = .init(rawValue: 1) + public static let testFlightBeta: AudienceType = .init(rawValue: 2) + public static let any: AudienceType = .init(rawValue: .max) + public static let `default`: AudienceType = [.testFlightBeta, proSubscriber] + public static let none: AudienceType = [] +} + +struct MockFeatureFlag: FeatureFlag { + static let initialValue: Int = .random(in: 1_000 ... 9_999) + + typealias UserTypeValue = AudienceType + + static let probability: Double = .random(in: 0 ..< 1) +} + +final class FeatureTests: XCTestCase { + func testExample() throws { + let key = UUID().uuidString + let expectedValue = Int.random(in: 100 ... 1_000) + let feature = Feature( + key: key, + defaultValue: 0, + userType: AudienceType.default + ) + let fullKey = [ + FeatureFlags.rootKey, key, FeatureFlags.valueKey + ].joined(separator: ".") + feature.value.wrappedValue = expectedValue + let actualValue = UserDefaults.standard.integer(forKey: fullKey) + XCTAssertEqual(actualValue, expectedValue) + } + + func testMockFlag() { + XCTAssertEqual(MockFeatureFlag.key, "Mock") + let defaultMock = MockFeatureFlag.defaultValue +// XCTAssertEqual( +// defaultMock.value.wrappedValue, MockFeatureFlag.initialValue +// ) + } +} From 99b5e2676fc6218f988b6e6c902f4a4c7947f1e3 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 7 May 2024 09:32:35 -0500 Subject: [PATCH 11/27] fixing feather --- .../FeatureAvailabilityMetrics.swift | 13 ++++++++----- Sources/FeatherQuill/FeatureValue.swift | 10 +++++----- Tests/FeatherQuillTests/FeatureTests.swift | 16 +++++++++++++--- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift index efe07df..cf83e62 100644 --- a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift +++ b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift @@ -34,21 +34,24 @@ internal struct FeatureAvailabilityMetrics: Equatable { private let probability: Double fileprivate var value: Double { - Double(userType.rawValue) + probability.remainder(dividingBy: 1) + Double(userType.rawValue) + probability.truncatingRemainder(dividingBy: 1) } fileprivate init(value: Double) { let rawValueDouble = floor(value) let rawValue = UserTypeValue.RawValue(rawValueDouble) - let probability = ((value - rawValueDouble) * 1_000).rounded() / 1_000.0 + let probability = (value - rawValueDouble) self.init(userType: .init(rawValue: rawValue), probability: probability) } + + static internal func roundProbability (_ value: Double) -> Double { + assert(value <= 1.0) + return ((value) * 1_000).rounded() / 1_000.0 + } internal init(userType: UserTypeValue, probability: Double) { self.userType = userType - self.probability = probability - // assert((probability * 1_000).rounded() / 1_000.0 == probability) - assert(probability <= 1.0) + self.probability = Self.roundProbability(probability) } internal func calculateAvailability() -> Bool { diff --git a/Sources/FeatherQuill/FeatureValue.swift b/Sources/FeatherQuill/FeatureValue.swift index 7703701..bad3295 100644 --- a/Sources/FeatherQuill/FeatureValue.swift +++ b/Sources/FeatherQuill/FeatureValue.swift @@ -40,15 +40,15 @@ private let fullKey: String public var value: Binding { .init { - self._isEnabled + self._value } set: { value in - self._isEnabled = value + self._value = value } } - private var _isEnabled: ValueType { + private var _value: ValueType { didSet { - userDefaults.setValue(_isEnabled, forKey: fullKey) + userDefaults.setValue(_value, forKey: fullKey) } } @@ -71,7 +71,7 @@ userDefaults.setValue(defaultValue, forKey: fullKey) initialValue = defaultValue } - _isEnabled = initialValue + _value = initialValue } } #endif diff --git a/Tests/FeatherQuillTests/FeatureTests.swift b/Tests/FeatherQuillTests/FeatureTests.swift index 08566af..1ae75f0 100644 --- a/Tests/FeatherQuillTests/FeatureTests.swift +++ b/Tests/FeatherQuillTests/FeatureTests.swift @@ -79,12 +79,22 @@ final class FeatureTests: XCTestCase { let actualValue = UserDefaults.standard.integer(forKey: fullKey) XCTAssertEqual(actualValue, expectedValue) } + + func testUserDefaultsMetrics() { + let expected = FeatureAvailabilityMetrics(userType: AudienceType.proSubscriber, probability: .random(in: 0..<1)) + UserDefaults.standard.set(expected, forKey: "testMetrics") + let actual : FeatureAvailabilityMetrics? = UserDefaults.standard.metrics(forKey: "testMetrics") + + XCTAssertEqual(expected, actual) + } func testMockFlag() { + let domain = Bundle.main.bundleIdentifier! + UserDefaults.standard.removePersistentDomain(forName: domain) XCTAssertEqual(MockFeatureFlag.key, "Mock") let defaultMock = MockFeatureFlag.defaultValue -// XCTAssertEqual( -// defaultMock.value.wrappedValue, MockFeatureFlag.initialValue -// ) + XCTAssertEqual( + defaultMock.value.wrappedValue, MockFeatureFlag.initialValue + ) } } From b2c715785ec389bedc1e8b03f16f9d8c3b91f503 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 7 May 2024 09:40:52 -0500 Subject: [PATCH 12/27] fixing tests and linting --- .../FeatherQuill/FeatureAvailability.swift | 2 - .../FeatureAvailabilityMetrics.swift | 11 ++-- Tests/FeatherQuillTests/AudienceType.swift | 54 +++++++++++++++++++ .../FeatureAvailabilityMetricsTests.swift | 41 ++++++++++++++ .../FeatherQuillTests/FeatureFlagTests.swift | 42 +++++++++++++++ Tests/FeatherQuillTests/FeatureTests.swift | 51 ------------------ ...QuillTests.swift => MockFeatureFlag.swift} | 17 +++--- 7 files changed, 149 insertions(+), 69 deletions(-) create mode 100644 Tests/FeatherQuillTests/AudienceType.swift create mode 100644 Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift create mode 100644 Tests/FeatherQuillTests/FeatureFlagTests.swift rename Tests/FeatherQuillTests/{FeatherQuillTests.swift => MockFeatureFlag.swift} (76%) diff --git a/Sources/FeatherQuill/FeatureAvailability.swift b/Sources/FeatherQuill/FeatureAvailability.swift index 2f6a44e..da6e268 100644 --- a/Sources/FeatherQuill/FeatureAvailability.swift +++ b/Sources/FeatherQuill/FeatureAvailability.swift @@ -111,12 +111,10 @@ internal struct FeatureAvailability { } let value = metrics.calculateAvailability() - print("Updating Availability: \(value)") userDefaults.set(value, forKey: availabilityKey) } private func initialize() { - // check for availablity let metricsHaveChanged = initializeMetrics() initializeAvailability(force: metricsHaveChanged) } diff --git a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift index cf83e62..6f41f23 100644 --- a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift +++ b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift @@ -43,24 +43,23 @@ internal struct FeatureAvailabilityMetrics: Equatable { let probability = (value - rawValueDouble) self.init(userType: .init(rawValue: rawValue), probability: probability) } - - static internal func roundProbability (_ value: Double) -> Double { - assert(value <= 1.0) - return ((value) * 1_000).rounded() / 1_000.0 - } internal init(userType: UserTypeValue, probability: Double) { self.userType = userType self.probability = Self.roundProbability(probability) } + internal static func roundProbability(_ value: Double) -> Double { + assert(value <= 1.0) + return (value * 1_000).rounded() / 1_000.0 + } + internal func calculateAvailability() -> Bool { let value: Bool if UserTypeValue.includes(userType) { value = true } else { let randomValue: Double = .random(in: 0.0 ..< 1.0) - print("Random Value: \(randomValue)") value = randomValue <= probability } return value diff --git a/Tests/FeatherQuillTests/AudienceType.swift b/Tests/FeatherQuillTests/AudienceType.swift new file mode 100644 index 0000000..a22b97d --- /dev/null +++ b/Tests/FeatherQuillTests/AudienceType.swift @@ -0,0 +1,54 @@ +// +// AudienceType.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import FeatherQuill + +public struct AudienceType: UserType { + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static func includes(_ value: AudienceType) -> Bool { + guard value.rawValue > 0 else { + return false + } + let value: Bool = .random() + return value + } + + public var rawValue: Int + + public typealias RawValue = Int + + public static let proSubscriber: AudienceType = .init(rawValue: 1) + public static let testFlightBeta: AudienceType = .init(rawValue: 2) + public static let any: AudienceType = .init(rawValue: .max) + public static let `default`: AudienceType = [.testFlightBeta, proSubscriber] + public static let none: AudienceType = [] +} diff --git a/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift b/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift new file mode 100644 index 0000000..97a640d --- /dev/null +++ b/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift @@ -0,0 +1,41 @@ +// +// FeatureAvailabilityMetricsTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +@testable import FeatherQuill +import XCTest + +final class FeatureAvailabilityMetricsTests: XCTestCase { + func testUserDefaultsMetrics() { + let expected = FeatureAvailabilityMetrics(userType: AudienceType.proSubscriber, probability: .random(in: 0 ..< 1)) + UserDefaults.standard.set(expected, forKey: "testMetrics") + let actual: FeatureAvailabilityMetrics? = UserDefaults.standard.metrics(forKey: "testMetrics") + + XCTAssertEqual(expected, actual) + } +} diff --git a/Tests/FeatherQuillTests/FeatureFlagTests.swift b/Tests/FeatherQuillTests/FeatureFlagTests.swift new file mode 100644 index 0000000..6c4c402 --- /dev/null +++ b/Tests/FeatherQuillTests/FeatureFlagTests.swift @@ -0,0 +1,42 @@ +// +// FeatureFlagTests.swift +// SimulatorServices +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import XCTest + +final class FeatureFlagTests: XCTestCase { + func testMockFlag() { + let domain = Bundle.main.bundleIdentifier! + UserDefaults.standard.removePersistentDomain(forName: domain) + XCTAssertEqual(MockFeatureFlag.key, "Mock") + let defaultMock = MockFeatureFlag.defaultValue + XCTAssertEqual( + defaultMock.value.wrappedValue, MockFeatureFlag.initialValue + ) + } +} diff --git a/Tests/FeatherQuillTests/FeatureTests.swift b/Tests/FeatherQuillTests/FeatureTests.swift index 1ae75f0..ac69433 100644 --- a/Tests/FeatherQuillTests/FeatureTests.swift +++ b/Tests/FeatherQuillTests/FeatureTests.swift @@ -30,39 +30,6 @@ @testable import FeatherQuill import XCTest -public struct AudienceType: UserType { - public init(rawValue: Int) { - self.rawValue = rawValue - } - - public static func includes(_ value: AudienceType) -> Bool { - guard value.rawValue > 0 else { - return false - } - let value: Bool = .random() - print("User Matches: \(value)") - return value - } - - public var rawValue: Int - - public typealias RawValue = Int - - public static let proSubscriber: AudienceType = .init(rawValue: 1) - public static let testFlightBeta: AudienceType = .init(rawValue: 2) - public static let any: AudienceType = .init(rawValue: .max) - public static let `default`: AudienceType = [.testFlightBeta, proSubscriber] - public static let none: AudienceType = [] -} - -struct MockFeatureFlag: FeatureFlag { - static let initialValue: Int = .random(in: 1_000 ... 9_999) - - typealias UserTypeValue = AudienceType - - static let probability: Double = .random(in: 0 ..< 1) -} - final class FeatureTests: XCTestCase { func testExample() throws { let key = UUID().uuidString @@ -79,22 +46,4 @@ final class FeatureTests: XCTestCase { let actualValue = UserDefaults.standard.integer(forKey: fullKey) XCTAssertEqual(actualValue, expectedValue) } - - func testUserDefaultsMetrics() { - let expected = FeatureAvailabilityMetrics(userType: AudienceType.proSubscriber, probability: .random(in: 0..<1)) - UserDefaults.standard.set(expected, forKey: "testMetrics") - let actual : FeatureAvailabilityMetrics? = UserDefaults.standard.metrics(forKey: "testMetrics") - - XCTAssertEqual(expected, actual) - } - - func testMockFlag() { - let domain = Bundle.main.bundleIdentifier! - UserDefaults.standard.removePersistentDomain(forName: domain) - XCTAssertEqual(MockFeatureFlag.key, "Mock") - let defaultMock = MockFeatureFlag.defaultValue - XCTAssertEqual( - defaultMock.value.wrappedValue, MockFeatureFlag.initialValue - ) - } } diff --git a/Tests/FeatherQuillTests/FeatherQuillTests.swift b/Tests/FeatherQuillTests/MockFeatureFlag.swift similarity index 76% rename from Tests/FeatherQuillTests/FeatherQuillTests.swift rename to Tests/FeatherQuillTests/MockFeatureFlag.swift index f462dea..c44c73d 100644 --- a/Tests/FeatherQuillTests/FeatherQuillTests.swift +++ b/Tests/FeatherQuillTests/MockFeatureFlag.swift @@ -1,5 +1,5 @@ // -// FeatherQuillTests.swift +// MockFeatureFlag.swift // SimulatorServices // // Created by Leo Dion. @@ -27,15 +27,12 @@ // OTHER DEALINGS IN THE SOFTWARE. // -@testable import FeatherQuill -import XCTest +import FeatherQuill -final class FeatherQuillTests: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest +struct MockFeatureFlag: FeatureFlag { + static let initialValue: Int = .random(in: 1_000 ... 9_999) - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } + typealias UserTypeValue = AudienceType + + static let probability: Double = .random(in: 0 ..< 1) } From 72f870bb73c3c3d01078cc50f6241c54604c2d36 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 7 May 2024 10:10:46 -0500 Subject: [PATCH 13/27] fixing tests --- .../FeatherQuillTests/FeatureFlagTests.swift | 21 +++++++----- Tests/FeatherQuillTests/FeatureTests.swift | 33 +++++++++++-------- Tests/FeatherQuillTests/MockFeatureFlag.swift | 14 ++++---- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/Tests/FeatherQuillTests/FeatureFlagTests.swift b/Tests/FeatherQuillTests/FeatureFlagTests.swift index 6c4c402..525fd3d 100644 --- a/Tests/FeatherQuillTests/FeatureFlagTests.swift +++ b/Tests/FeatherQuillTests/FeatureFlagTests.swift @@ -30,13 +30,18 @@ import XCTest final class FeatureFlagTests: XCTestCase { - func testMockFlag() { - let domain = Bundle.main.bundleIdentifier! - UserDefaults.standard.removePersistentDomain(forName: domain) - XCTAssertEqual(MockFeatureFlag.key, "Mock") - let defaultMock = MockFeatureFlag.defaultValue - XCTAssertEqual( - defaultMock.value.wrappedValue, MockFeatureFlag.initialValue - ) + func testFlag() throws { + #if canImport(SwiftUI) + let domain = Bundle.main.bundleIdentifier! + UserDefaults.standard.removePersistentDomain(forName: domain) + XCTAssertEqual(MockFeatureFlag.key, "Mock") + let defaultMock = MockFeatureFlag.defaultValue + XCTAssertEqual( + defaultMock.value.wrappedValue, MockFeatureFlag.initialValue + ) + #else + throw XCTSkip("Not suported outside of SwiftUI.") + + #endif } } diff --git a/Tests/FeatherQuillTests/FeatureTests.swift b/Tests/FeatherQuillTests/FeatureTests.swift index ac69433..c0a9f87 100644 --- a/Tests/FeatherQuillTests/FeatureTests.swift +++ b/Tests/FeatherQuillTests/FeatureTests.swift @@ -31,19 +31,24 @@ import XCTest final class FeatureTests: XCTestCase { - func testExample() throws { - let key = UUID().uuidString - let expectedValue = Int.random(in: 100 ... 1_000) - let feature = Feature( - key: key, - defaultValue: 0, - userType: AudienceType.default - ) - let fullKey = [ - FeatureFlags.rootKey, key, FeatureFlags.valueKey - ].joined(separator: ".") - feature.value.wrappedValue = expectedValue - let actualValue = UserDefaults.standard.integer(forKey: fullKey) - XCTAssertEqual(actualValue, expectedValue) + func testWrapped() throws { + #if canImport(SwiftUI) + let key = UUID().uuidString + let expectedValue = Int.random(in: 100 ... 1_000) + let feature = Feature( + key: key, + defaultValue: 0, + userType: AudienceType.default + ) + let fullKey = [ + FeatureFlags.rootKey, key, FeatureFlags.valueKey + ].joined(separator: ".") + feature.value.wrappedValue = expectedValue + let actualValue = UserDefaults.standard.integer(forKey: fullKey) + XCTAssertEqual(actualValue, expectedValue) + #else + throw XCTSkip("Not suported outside of SwiftUI.") + + #endif } } diff --git a/Tests/FeatherQuillTests/MockFeatureFlag.swift b/Tests/FeatherQuillTests/MockFeatureFlag.swift index c44c73d..09dc5a0 100644 --- a/Tests/FeatherQuillTests/MockFeatureFlag.swift +++ b/Tests/FeatherQuillTests/MockFeatureFlag.swift @@ -27,12 +27,14 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import FeatherQuill +#if canImport(SwiftUI) + import FeatherQuill -struct MockFeatureFlag: FeatureFlag { - static let initialValue: Int = .random(in: 1_000 ... 9_999) + struct MockFeatureFlag: FeatureFlag { + static let initialValue: Int = .random(in: 1_000 ... 9_999) - typealias UserTypeValue = AudienceType + typealias UserTypeValue = AudienceType - static let probability: Double = .random(in: 0 ..< 1) -} + static let probability: Double = .random(in: 0 ..< 1) + } +#endif From 0e088e89a80135670d069a7d0e3e66862e99e7a3 Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 7 May 2024 10:36:42 -0500 Subject: [PATCH 14/27] Update FeatherQuill.yml --- .github/workflows/FeatherQuill.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/FeatherQuill.yml b/.github/workflows/FeatherQuill.yml index db3dc96..57d992c 100644 --- a/.github/workflows/FeatherQuill.yml +++ b/.github/workflows/FeatherQuill.yml @@ -74,12 +74,12 @@ jobs: strategy: matrix: include: - - xcode: "/Applications/Xcode_15.0.1.app" - os: macos-13 - iOSVersion: "17.0.1" - watchOSVersion: "10.0" - watchName: "Apple Watch Series 9 (41mm)" - iPhoneName: "iPhone 15" + # - xcode: "/Applications/Xcode_15.0.1.app" + # os: macos-13 + # iOSVersion: "17.0.1" + # watchOSVersion: "10.0" + # watchName: "Apple Watch Series 9 (41mm)" + # iPhoneName: "iPhone 15" - xcode: "/Applications/Xcode_15.1.app" os: macos-13 iOSVersion: "17.2" From ab79fcc8b87e5c4cb78dfc649d4218882288f8b5 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 7 May 2024 11:42:54 -0500 Subject: [PATCH 15/27] improve linting --- .swiftformat | 4 +-- .swiftlint.yml | 36 ++++++++++++------- Demo/FeatureFlagsApp/ContentView.swift | 2 +- Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift | 2 +- .../FeatureFlagsExample/AudienceType.swift | 2 +- .../NewDesignFeature.swift | 2 +- .../FeatureFlagsExampleTests.swift | 2 +- Package.swift | 12 ++++++- .../FeatherQuill/AvailabilityOptions.swift | 2 +- Sources/FeatherQuill/Feature.swift | 2 +- .../FeatherQuill/FeatureAvailability.swift | 4 +-- .../FeatureAvailabilityMetrics.swift | 2 +- Sources/FeatherQuill/FeatureFlag.swift | 2 +- Sources/FeatherQuill/FeatureFlags.swift | 2 +- Sources/FeatherQuill/FeatureValue.swift | 2 +- Sources/FeatherQuill/UserType.swift | 2 +- Tests/FeatherQuillTests/AudienceType.swift | 2 +- .../FeatureAvailabilityMetricsTests.swift | 2 +- .../FeatherQuillTests/FeatureFlagTests.swift | 2 +- Tests/FeatherQuillTests/FeatureTests.swift | 2 +- Tests/FeatherQuillTests/MockFeatureFlag.swift | 2 +- 21 files changed, 56 insertions(+), 34 deletions(-) diff --git a/.swiftformat b/.swiftformat index c510d49..6729548 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,7 +1,7 @@ --indent 2 ---header "\n .*?\.swift\n SimulatorServices\n\n Created by Leo Dion.\n Copyright © {year} BrightDigit.\n\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the “Software”), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n \n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n" +--header "\n .*?\.swift\n FeatherQuill\n\n Created by Leo Dion.\n Copyright © {year} BrightDigit.\n\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the “Software”), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n \n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n" --commas inline ---disable wrapMultilineStatementBraces, redundantInternal +--disable wrapMultilineStatementBraces, redundantInternal,redundantSelf,wrapMultilineStatementBraces,genericExtensions --extensionacl on-declarations --decimalgrouping 3,4 --exclude .build, DerivedData, .swiftpm diff --git a/.swiftlint.yml b/.swiftlint.yml index ca1014e..30ba9c5 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,6 +1,5 @@ opt_in_rules: - array_init - - attributes - closure_body_length - closure_end_indentation - closure_spacing @@ -22,7 +21,7 @@ opt_in_rules: - explicit_acl - explicit_init - explicit_top_level_acl - - fallthrough + # - fallthrough - fatal_error_message - file_name - file_name_no_space @@ -30,7 +29,7 @@ opt_in_rules: - first_where - flatmap_over_map_reduce - force_unwrapping - - function_default_parameter_at_end +# - function_default_parameter_at_end - ibinspectable_in_extension - identical_operands - implicit_return @@ -42,7 +41,7 @@ opt_in_rules: - legacy_random - literal_expression_end_indentation - lower_acl_than_parent - # - missing_docs +# - missing_docs - modifier_order - multiline_arguments - multiline_arguments_brackets @@ -78,7 +77,7 @@ opt_in_rules: - static_operator - strong_iboutlet - toggle_bool - - trailing_closure +# - trailing_closure - type_contents_order - unavailable_function - unneeded_parentheses_in_closure_argument @@ -90,30 +89,43 @@ opt_in_rules: - xct_specific_matcher - yoda_condition analyzer_rules: - - explicit_self - - unused_declaration - unused_import + - unused_declaration +cyclomatic_complexity: + - 6 + - 12 type_body_length: - 100 - 200 file_length: - - 200 - - 300 + warning: 215 + error: 300 function_body_length: - 18 - 40 function_parameter_count: 8 line_length: - - 90 - - 90 + - 108 + - 200 +closure_body_length: + - 50 + - 60 identifier_name: excluded: - id + - no excluded: - - Tests - DerivedData - .build - .swiftpm - Demo indentation_width: indentation_width: 2 +file_name: + severity: error +fatal_error_message: + severity: error +disabled_rules: + - nesting + - implicit_getter + - switch_case_alignment \ No newline at end of file diff --git a/Demo/FeatureFlagsApp/ContentView.swift b/Demo/FeatureFlagsApp/ContentView.swift index df9c915..0587a63 100644 --- a/Demo/FeatureFlagsApp/ContentView.swift +++ b/Demo/FeatureFlagsApp/ContentView.swift @@ -1,6 +1,6 @@ // // ContentView.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift b/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift index 71a0a7b..ec11d23 100644 --- a/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift +++ b/Demo/FeatureFlagsApp/FeatureFlagsAppApp.swift @@ -1,6 +1,6 @@ // // FeatureFlagsAppApp.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift index 5574693..c94532f 100644 --- a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift +++ b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift @@ -1,6 +1,6 @@ // // AudienceType.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift index 93d689d..1890ba6 100644 --- a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift +++ b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift @@ -1,6 +1,6 @@ // // NewDesignFeature.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift b/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift index f4a55e8..7f08fce 100644 --- a/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift +++ b/Demo/FeatureFlagsExample/Tests/FeatureFlagsExampleTests/FeatureFlagsExampleTests.swift @@ -1,6 +1,6 @@ // // FeatureFlagsExampleTests.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Package.swift b/Package.swift index a19980c..950341e 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,17 @@ let package = Package( ], targets: [ .target( - name: "FeatherQuill" + name: "FeatherQuill", + swiftSettings: [ + SwiftSetting.enableUpcomingFeature("BareSlashRegexLiterals"), + SwiftSetting.enableUpcomingFeature("ConciseMagicFile"), + SwiftSetting.enableUpcomingFeature("ExistentialAny"), + SwiftSetting.enableUpcomingFeature("ForwardTrailingClosures"), + SwiftSetting.enableUpcomingFeature("ImplicitOpenExistentials"), + SwiftSetting.enableUpcomingFeature("DisableOutwardActorInference"), + SwiftSetting.enableExperimentalFeature("StrictConcurrency"), + SwiftSetting.unsafeFlags(["-warn-concurrency", "-enable-actor-data-race-checks"]) + ] ), .testTarget( name: "FeatherQuillTests", diff --git a/Sources/FeatherQuill/AvailabilityOptions.swift b/Sources/FeatherQuill/AvailabilityOptions.swift index 4c90718..51ecbfc 100644 --- a/Sources/FeatherQuill/AvailabilityOptions.swift +++ b/Sources/FeatherQuill/AvailabilityOptions.swift @@ -1,6 +1,6 @@ // // AvailabilityOptions.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Sources/FeatherQuill/Feature.swift b/Sources/FeatherQuill/Feature.swift index d220e31..61f3931 100644 --- a/Sources/FeatherQuill/Feature.swift +++ b/Sources/FeatherQuill/Feature.swift @@ -1,6 +1,6 @@ // // Feature.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Sources/FeatherQuill/FeatureAvailability.swift b/Sources/FeatherQuill/FeatureAvailability.swift index da6e268..d2c4428 100644 --- a/Sources/FeatherQuill/FeatureAvailability.swift +++ b/Sources/FeatherQuill/FeatureAvailability.swift @@ -1,6 +1,6 @@ // // FeatureAvailability.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. @@ -101,7 +101,7 @@ internal struct FeatureAvailability { switch (isAvailable, force, options.contains(.allowOverwriteAvailable)) { case (true, _, false): return - case (.some(_), false, _): + case (.some, false, _): return case (.none, _, _): diff --git a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift index 6f41f23..18aa629 100644 --- a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift +++ b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift @@ -1,6 +1,6 @@ // // FeatureAvailabilityMetrics.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Sources/FeatherQuill/FeatureFlag.swift b/Sources/FeatherQuill/FeatureFlag.swift index 2d6de89..830e0ef 100644 --- a/Sources/FeatherQuill/FeatureFlag.swift +++ b/Sources/FeatherQuill/FeatureFlag.swift @@ -1,6 +1,6 @@ // // FeatureFlag.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Sources/FeatherQuill/FeatureFlags.swift b/Sources/FeatherQuill/FeatureFlags.swift index 6866481..acfdf0d 100644 --- a/Sources/FeatherQuill/FeatureFlags.swift +++ b/Sources/FeatherQuill/FeatureFlags.swift @@ -1,6 +1,6 @@ // // FeatureFlags.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Sources/FeatherQuill/FeatureValue.swift b/Sources/FeatherQuill/FeatureValue.swift index bad3295..69d4491 100644 --- a/Sources/FeatherQuill/FeatureValue.swift +++ b/Sources/FeatherQuill/FeatureValue.swift @@ -1,6 +1,6 @@ // // FeatureValue.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Sources/FeatherQuill/UserType.swift b/Sources/FeatherQuill/UserType.swift index a651c9b..31bbf82 100644 --- a/Sources/FeatherQuill/UserType.swift +++ b/Sources/FeatherQuill/UserType.swift @@ -1,6 +1,6 @@ // // UserType.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Tests/FeatherQuillTests/AudienceType.swift b/Tests/FeatherQuillTests/AudienceType.swift index a22b97d..13dd52a 100644 --- a/Tests/FeatherQuillTests/AudienceType.swift +++ b/Tests/FeatherQuillTests/AudienceType.swift @@ -1,6 +1,6 @@ // // AudienceType.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift b/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift index 97a640d..c4f9f68 100644 --- a/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift +++ b/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift @@ -1,6 +1,6 @@ // // FeatureAvailabilityMetricsTests.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Tests/FeatherQuillTests/FeatureFlagTests.swift b/Tests/FeatherQuillTests/FeatureFlagTests.swift index 525fd3d..5ac6f80 100644 --- a/Tests/FeatherQuillTests/FeatureFlagTests.swift +++ b/Tests/FeatherQuillTests/FeatureFlagTests.swift @@ -1,6 +1,6 @@ // // FeatureFlagTests.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Tests/FeatherQuillTests/FeatureTests.swift b/Tests/FeatherQuillTests/FeatureTests.swift index c0a9f87..e61c143 100644 --- a/Tests/FeatherQuillTests/FeatureTests.swift +++ b/Tests/FeatherQuillTests/FeatureTests.swift @@ -1,6 +1,6 @@ // // FeatureTests.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. diff --git a/Tests/FeatherQuillTests/MockFeatureFlag.swift b/Tests/FeatherQuillTests/MockFeatureFlag.swift index 09dc5a0..b27b0ab 100644 --- a/Tests/FeatherQuillTests/MockFeatureFlag.swift +++ b/Tests/FeatherQuillTests/MockFeatureFlag.swift @@ -1,6 +1,6 @@ // // MockFeatureFlag.swift -// SimulatorServices +// FeatherQuill // // Created by Leo Dion. // Copyright © 2024 BrightDigit. From 89c33f6fad057cad172337c59fb7f5a77ce3d4c4 Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 7 May 2024 13:04:46 -0500 Subject: [PATCH 16/27] Update FeatherQuill.yml --- .github/workflows/FeatherQuill.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/FeatherQuill.yml b/.github/workflows/FeatherQuill.yml index 57d992c..1ab2672 100644 --- a/.github/workflows/FeatherQuill.yml +++ b/.github/workflows/FeatherQuill.yml @@ -80,12 +80,12 @@ jobs: # watchOSVersion: "10.0" # watchName: "Apple Watch Series 9 (41mm)" # iPhoneName: "iPhone 15" - - xcode: "/Applications/Xcode_15.1.app" - os: macos-13 - iOSVersion: "17.2" - watchOSVersion: "10.2" - watchName: "Apple Watch Series 9 (45mm)" - iPhoneName: "iPhone 15 Plus" + # - xcode: "/Applications/Xcode_15.1.app" + # os: macos-13 + # iOSVersion: "17.2" + # watchOSVersion: "10.2" + # watchName: "Apple Watch Series 9 (45mm)" + # iPhoneName: "iPhone 15 Plus" - xcode: "/Applications/Xcode_15.2.app" os: macos-14 iOSVersion: "17.2" From c6a079797c878aaecbe08ff4dd3d179bf1355627 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 7 May 2024 13:41:44 -0500 Subject: [PATCH 17/27] working on FeatherQuill --- Sources/FeatherQuill/AvailabilityOptions.swift | 2 +- Sources/FeatherQuill/FeatureFlag.swift | 6 ++++-- Tests/FeatherQuillTests/AudienceType.swift | 18 ++++++++---------- .../FeatureAvailabilityMetricsTests.swift | 12 ++++++++---- Tests/FeatherQuillTests/FeatureFlagTests.swift | 5 +++-- Tests/FeatherQuillTests/FeatureTests.swift | 4 ++-- Tests/FeatherQuillTests/MockFeatureFlag.swift | 8 ++++---- 7 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Sources/FeatherQuill/AvailabilityOptions.swift b/Sources/FeatherQuill/AvailabilityOptions.swift index 51ecbfc..94ce8ab 100644 --- a/Sources/FeatherQuill/AvailabilityOptions.swift +++ b/Sources/FeatherQuill/AvailabilityOptions.swift @@ -29,7 +29,7 @@ import Foundation -public struct AvailabilityOptions: OptionSet { +public struct AvailabilityOptions: OptionSet, Sendable { public typealias RawValue = Int public static let `default`: AvailabilityOptions = .init() diff --git a/Sources/FeatherQuill/FeatureFlag.swift b/Sources/FeatherQuill/FeatureFlag.swift index 830e0ef..8181af1 100644 --- a/Sources/FeatherQuill/FeatureFlag.swift +++ b/Sources/FeatherQuill/FeatureFlag.swift @@ -48,10 +48,12 @@ } public protocol FeatureFlag: EnvironmentKey - where Value == Feature { + where Value == FeatherQuill.Feature { associatedtype ValueType = Bool associatedtype UserTypeValue: UserType + typealias Feature = FeatherQuill.Feature + static var key: String { get } static var audience: UserTypeValue { get } static var probability: Double { get } @@ -76,7 +78,7 @@ FeatureFlagSuffixes.key(from: typeName) } - public static var defaultValue: Feature { + public static var defaultValue: FeatherQuill.Feature { .init( key: key, defaultValue: initialValue, diff --git a/Tests/FeatherQuillTests/AudienceType.swift b/Tests/FeatherQuillTests/AudienceType.swift index 13dd52a..6cbef23 100644 --- a/Tests/FeatherQuillTests/AudienceType.swift +++ b/Tests/FeatherQuillTests/AudienceType.swift @@ -30,6 +30,14 @@ import FeatherQuill public struct AudienceType: UserType { + public typealias RawValue = Int + public static let proSubscriber: AudienceType = .init(rawValue: 1) + public static let testFlightBeta: AudienceType = .init(rawValue: 2) + public static let any: AudienceType = .init(rawValue: .max) + public static let `default`: AudienceType = [.testFlightBeta, proSubscriber] + public static let none: AudienceType = [] + public var rawValue: Int + public init(rawValue: Int) { self.rawValue = rawValue } @@ -41,14 +49,4 @@ public struct AudienceType: UserType { let value: Bool = .random() return value } - - public var rawValue: Int - - public typealias RawValue = Int - - public static let proSubscriber: AudienceType = .init(rawValue: 1) - public static let testFlightBeta: AudienceType = .init(rawValue: 2) - public static let any: AudienceType = .init(rawValue: .max) - public static let `default`: AudienceType = [.testFlightBeta, proSubscriber] - public static let none: AudienceType = [] } diff --git a/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift b/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift index c4f9f68..c80ac11 100644 --- a/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift +++ b/Tests/FeatherQuillTests/FeatureAvailabilityMetricsTests.swift @@ -30,11 +30,15 @@ @testable import FeatherQuill import XCTest -final class FeatureAvailabilityMetricsTests: XCTestCase { - func testUserDefaultsMetrics() { - let expected = FeatureAvailabilityMetrics(userType: AudienceType.proSubscriber, probability: .random(in: 0 ..< 1)) +internal final class FeatureAvailabilityMetricsTests: XCTestCase { + internal func testUserDefaultsMetrics() { + let expected = FeatureAvailabilityMetrics( + userType: AudienceType.proSubscriber, + probability: .random(in: 0 ..< 1) + ) UserDefaults.standard.set(expected, forKey: "testMetrics") - let actual: FeatureAvailabilityMetrics? = UserDefaults.standard.metrics(forKey: "testMetrics") + let actual: FeatureAvailabilityMetrics? = + UserDefaults.standard.metrics(forKey: "testMetrics") XCTAssertEqual(expected, actual) } diff --git a/Tests/FeatherQuillTests/FeatureFlagTests.swift b/Tests/FeatherQuillTests/FeatureFlagTests.swift index 5ac6f80..4cb5cab 100644 --- a/Tests/FeatherQuillTests/FeatureFlagTests.swift +++ b/Tests/FeatherQuillTests/FeatureFlagTests.swift @@ -29,9 +29,10 @@ import XCTest -final class FeatureFlagTests: XCTestCase { - func testFlag() throws { +internal final class FeatureFlagTests: XCTestCase { + internal func testFlag() throws { #if canImport(SwiftUI) + // swiftlint:disable:next force_unwrapping let domain = Bundle.main.bundleIdentifier! UserDefaults.standard.removePersistentDomain(forName: domain) XCTAssertEqual(MockFeatureFlag.key, "Mock") diff --git a/Tests/FeatherQuillTests/FeatureTests.swift b/Tests/FeatherQuillTests/FeatureTests.swift index e61c143..a66e8a3 100644 --- a/Tests/FeatherQuillTests/FeatureTests.swift +++ b/Tests/FeatherQuillTests/FeatureTests.swift @@ -30,8 +30,8 @@ @testable import FeatherQuill import XCTest -final class FeatureTests: XCTestCase { - func testWrapped() throws { +internal final class FeatureTests: XCTestCase { + internal func testWrapped() throws { #if canImport(SwiftUI) let key = UUID().uuidString let expectedValue = Int.random(in: 100 ... 1_000) diff --git a/Tests/FeatherQuillTests/MockFeatureFlag.swift b/Tests/FeatherQuillTests/MockFeatureFlag.swift index b27b0ab..e6003b1 100644 --- a/Tests/FeatherQuillTests/MockFeatureFlag.swift +++ b/Tests/FeatherQuillTests/MockFeatureFlag.swift @@ -30,11 +30,11 @@ #if canImport(SwiftUI) import FeatherQuill - struct MockFeatureFlag: FeatureFlag { - static let initialValue: Int = .random(in: 1_000 ... 9_999) + internal struct MockFeatureFlag: FeatureFlag { + internal typealias UserTypeValue = AudienceType - typealias UserTypeValue = AudienceType + internal static let initialValue: Int = .random(in: 1_000 ... 9_999) - static let probability: Double = .random(in: 0 ..< 1) + internal static let probability: Double = .random(in: 0 ..< 1) } #endif From 66a17ac710b40295197fc8de1d10ec374753f819 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 7 May 2024 14:37:20 -0500 Subject: [PATCH 18/27] Working through sendable stuff [skip ci] --- Sources/FeatherQuill/Feature.swift | 6 ++++-- Sources/FeatherQuill/FeatureAvailability.swift | 17 ++++++++++------- .../FeatureAvailabilityMetrics.swift | 4 ++-- Sources/FeatherQuill/FeatureFlag.swift | 6 +++++- Sources/FeatherQuill/UserType.swift | 1 + 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Sources/FeatherQuill/Feature.swift b/Sources/FeatherQuill/Feature.swift index 61f3931..4e3acb5 100644 --- a/Sources/FeatherQuill/Feature.swift +++ b/Sources/FeatherQuill/Feature.swift @@ -59,14 +59,16 @@ defaultValue: ValueType, userType: UserTypeValue, probability: Double = 0.0, - options: AvailabilityOptions = [] + options: AvailabilityOptions = [], + _ audienceCallback: @Sendable @escaping (UserTypeValue) async -> Bool ) { let value: FeatureValue = .init(key: key, defaultValue: defaultValue) let availablity: FeatureAvailability = .init( key: key, userType: userType, probability: probability, - options: options + options: options, + audienceCallback ) self.init(value: value, availability: availablity) } diff --git a/Sources/FeatherQuill/FeatureAvailability.swift b/Sources/FeatherQuill/FeatureAvailability.swift index d2c4428..aeae1ba 100644 --- a/Sources/FeatherQuill/FeatureAvailability.swift +++ b/Sources/FeatherQuill/FeatureAvailability.swift @@ -60,7 +60,8 @@ internal struct FeatureAvailability { userType: UserTypeValue, probability: Double = 0.0, userDefaults: UserDefaults = .standard, - options: AvailabilityOptions = [] + options: AvailabilityOptions = [], + _ availability: @Sendable @escaping (UserTypeValue) async -> Bool ) { let metricsKey = [ FeatureFlags.rootKey, key, FeatureFlags.metricsKey @@ -75,7 +76,7 @@ internal struct FeatureAvailability { options: options, metrics: .init(userType: userType, probability: probability) ) - initialize() + initialize(with: availability) } private func initializeMetrics() -> Bool { @@ -94,7 +95,7 @@ internal struct FeatureAvailability { return true } - private func initializeAvailability(force: Bool = false) { + private func initializeAvailability(with audienceCallback: @Sendable @escaping (UserTypeValue) async -> Bool, force: Bool = false) { let isAvailable = userDefaults.object(forKey: availabilityKey).map { _ in userDefaults.bool(forKey: availabilityKey) } @@ -110,12 +111,14 @@ internal struct FeatureAvailability { break } - let value = metrics.calculateAvailability() - userDefaults.set(value, forKey: availabilityKey) + Task { + let value = await metrics.calculateAvailability(audienceCallback) + userDefaults.set(value, forKey: availabilityKey) + } } - private func initialize() { + private func initialize(with audienceCallback: @Sendable @escaping (UserTypeValue) async -> Bool) { let metricsHaveChanged = initializeMetrics() - initializeAvailability(force: metricsHaveChanged) + initializeAvailability(with: audienceCallback, force: metricsHaveChanged) } } diff --git a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift index 18aa629..a3a7bc3 100644 --- a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift +++ b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift @@ -54,9 +54,9 @@ internal struct FeatureAvailabilityMetrics: Equatable { return (value * 1_000).rounded() / 1_000.0 } - internal func calculateAvailability() -> Bool { + internal func calculateAvailability(_ audienceCallback: @Sendable @escaping (UserTypeValue) async -> Bool) async -> Bool { let value: Bool - if UserTypeValue.includes(userType) { + if await audienceCallback(userType) { value = true } else { let randomValue: Double = .random(in: 0.0 ..< 1.0) diff --git a/Sources/FeatherQuill/FeatureFlag.swift b/Sources/FeatherQuill/FeatureFlag.swift index 8181af1..a772eb7 100644 --- a/Sources/FeatherQuill/FeatureFlag.swift +++ b/Sources/FeatherQuill/FeatureFlag.swift @@ -59,6 +59,9 @@ static var probability: Double { get } static var initialValue: ValueType { get } static var options: AvailabilityOptions { get } + + @Sendable + static func audienceCallback(_ userType: UserTypeValue) async -> Bool } extension FeatureFlag { @@ -83,7 +86,8 @@ key: key, defaultValue: initialValue, userType: audience, - probability: probability + probability: probability, + audienceCallback ) } } diff --git a/Sources/FeatherQuill/UserType.swift b/Sources/FeatherQuill/UserType.swift index 31bbf82..b873063 100644 --- a/Sources/FeatherQuill/UserType.swift +++ b/Sources/FeatherQuill/UserType.swift @@ -29,5 +29,6 @@ public protocol UserType: OptionSet where Self.RawValue: BinaryInteger { static var `default`: Self { get } + @available(*, deprecated) static func includes(_ userType: Self) -> Bool } From 9a205949921362521c63da2e062ecda757ce5981 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 7 May 2024 16:37:58 -0500 Subject: [PATCH 19/27] fixing feature value --- Sources/FeatherQuill/Feature.swift | 6 ++++- .../FeatherQuill/FeatureAvailability.swift | 5 ++++- .../FeatureAvailabilityMetrics.swift | 4 +++- Sources/FeatherQuill/FeatureValue.swift | 22 ++++++++++++++----- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/Sources/FeatherQuill/Feature.swift b/Sources/FeatherQuill/Feature.swift index 4e3acb5..9064527 100644 --- a/Sources/FeatherQuill/Feature.swift +++ b/Sources/FeatherQuill/Feature.swift @@ -36,7 +36,11 @@ private let featureValue: FeatureValue private let availability: FeatureAvailability - public var value: Binding { + public var bindingValue: Binding { + featureValue.bindingValue + } + + public var value: ValueType { featureValue.value } diff --git a/Sources/FeatherQuill/FeatureAvailability.swift b/Sources/FeatherQuill/FeatureAvailability.swift index aeae1ba..e9a677c 100644 --- a/Sources/FeatherQuill/FeatureAvailability.swift +++ b/Sources/FeatherQuill/FeatureAvailability.swift @@ -95,7 +95,10 @@ internal struct FeatureAvailability { return true } - private func initializeAvailability(with audienceCallback: @Sendable @escaping (UserTypeValue) async -> Bool, force: Bool = false) { + private func initializeAvailability( + with audienceCallback: @Sendable @escaping (UserTypeValue) async -> Bool, + force: Bool = false + ) { let isAvailable = userDefaults.object(forKey: availabilityKey).map { _ in userDefaults.bool(forKey: availabilityKey) } diff --git a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift index a3a7bc3..fe0a548 100644 --- a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift +++ b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift @@ -54,7 +54,9 @@ internal struct FeatureAvailabilityMetrics: Equatable { return (value * 1_000).rounded() / 1_000.0 } - internal func calculateAvailability(_ audienceCallback: @Sendable @escaping (UserTypeValue) async -> Bool) async -> Bool { + internal func calculateAvailability( + _ audienceCallback: @Sendable @escaping (UserTypeValue) async -> Bool + ) async -> Bool { let value: Bool if await audienceCallback(userType) { value = true diff --git a/Sources/FeatherQuill/FeatureValue.swift b/Sources/FeatherQuill/FeatureValue.swift index 69d4491..26d4c54 100644 --- a/Sources/FeatherQuill/FeatureValue.swift +++ b/Sources/FeatherQuill/FeatureValue.swift @@ -38,17 +38,27 @@ private let key: String private let defaultValue: ValueType private let fullKey: String - public var value: Binding { + public var bindingValue: Binding { .init { - self._value + self._storedValue } set: { value in - self._value = value + self._storedValue = value } } - private var _value: ValueType { + public var value: ValueType { + let value = userDefaults.value(forKey: fullKey) as? ValueType + print("get \(_storedValue)", value) + assert(value != nil) + return value ?? _storedValue + } + + private var _storedValue: ValueType { didSet { - userDefaults.setValue(_value, forKey: fullKey) + print("didSet \(_storedValue)") + userDefaults.setValue(_storedValue, forKey: fullKey) + userDefaults.synchronize() + print("set \(_storedValue)") } } @@ -71,7 +81,7 @@ userDefaults.setValue(defaultValue, forKey: fullKey) initialValue = defaultValue } - _value = initialValue + _storedValue = initialValue } } #endif From 8ea5055e2046ec11d03036a2f8bd67950e3bd332 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 8 May 2024 17:33:12 -0500 Subject: [PATCH 20/27] fixing FeatherQuill --- Sources/FeatherQuill/FeatureAvailability.swift | 4 +++- Sources/FeatherQuill/FeatureAvailabilityMetrics.swift | 2 +- Sources/FeatherQuill/FeatureValue.swift | 1 - Sources/FeatherQuill/UserType.swift | 2 +- Tests/FeatherQuillTests/FeatureFlagTests.swift | 2 +- Tests/FeatherQuillTests/FeatureTests.swift | 5 +++-- Tests/FeatherQuillTests/MockFeatureFlag.swift | 4 ++++ 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Sources/FeatherQuill/FeatureAvailability.swift b/Sources/FeatherQuill/FeatureAvailability.swift index e9a677c..939abf7 100644 --- a/Sources/FeatherQuill/FeatureAvailability.swift +++ b/Sources/FeatherQuill/FeatureAvailability.swift @@ -29,7 +29,7 @@ import Foundation -internal struct FeatureAvailability { +internal struct FeatureAvailability: Sendable { private let userDefaults: UserDefaults private let metricsKey: String private let availabilityKey: String @@ -125,3 +125,5 @@ internal struct FeatureAvailability { initializeAvailability(with: audienceCallback, force: metricsHaveChanged) } } + +extension UserDefaults: @unchecked Sendable {} diff --git a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift index fe0a548..d724563 100644 --- a/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift +++ b/Sources/FeatherQuill/FeatureAvailabilityMetrics.swift @@ -29,7 +29,7 @@ import Foundation -internal struct FeatureAvailabilityMetrics: Equatable { +internal struct FeatureAvailabilityMetrics: Equatable, Sendable { private let userType: UserTypeValue private let probability: Double diff --git a/Sources/FeatherQuill/FeatureValue.swift b/Sources/FeatherQuill/FeatureValue.swift index 26d4c54..e948f89 100644 --- a/Sources/FeatherQuill/FeatureValue.swift +++ b/Sources/FeatherQuill/FeatureValue.swift @@ -48,7 +48,6 @@ public var value: ValueType { let value = userDefaults.value(forKey: fullKey) as? ValueType - print("get \(_storedValue)", value) assert(value != nil) return value ?? _storedValue } diff --git a/Sources/FeatherQuill/UserType.swift b/Sources/FeatherQuill/UserType.swift index b873063..5a84520 100644 --- a/Sources/FeatherQuill/UserType.swift +++ b/Sources/FeatherQuill/UserType.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public protocol UserType: OptionSet where Self.RawValue: BinaryInteger { +public protocol UserType: OptionSet, Sendable where Self.RawValue: BinaryInteger { static var `default`: Self { get } @available(*, deprecated) static func includes(_ userType: Self) -> Bool diff --git a/Tests/FeatherQuillTests/FeatureFlagTests.swift b/Tests/FeatherQuillTests/FeatureFlagTests.swift index 4cb5cab..1555814 100644 --- a/Tests/FeatherQuillTests/FeatureFlagTests.swift +++ b/Tests/FeatherQuillTests/FeatureFlagTests.swift @@ -38,7 +38,7 @@ internal final class FeatureFlagTests: XCTestCase { XCTAssertEqual(MockFeatureFlag.key, "Mock") let defaultMock = MockFeatureFlag.defaultValue XCTAssertEqual( - defaultMock.value.wrappedValue, MockFeatureFlag.initialValue + defaultMock.value, MockFeatureFlag.initialValue ) #else throw XCTSkip("Not suported outside of SwiftUI.") diff --git a/Tests/FeatherQuillTests/FeatureTests.swift b/Tests/FeatherQuillTests/FeatureTests.swift index a66e8a3..c23e0da 100644 --- a/Tests/FeatherQuillTests/FeatureTests.swift +++ b/Tests/FeatherQuillTests/FeatureTests.swift @@ -39,11 +39,12 @@ internal final class FeatureTests: XCTestCase { key: key, defaultValue: 0, userType: AudienceType.default - ) + ) { _ in true } + let fullKey = [ FeatureFlags.rootKey, key, FeatureFlags.valueKey ].joined(separator: ".") - feature.value.wrappedValue = expectedValue + feature.bindingValue.wrappedValue = expectedValue let actualValue = UserDefaults.standard.integer(forKey: fullKey) XCTAssertEqual(actualValue, expectedValue) #else diff --git a/Tests/FeatherQuillTests/MockFeatureFlag.swift b/Tests/FeatherQuillTests/MockFeatureFlag.swift index e6003b1..2fe1321 100644 --- a/Tests/FeatherQuillTests/MockFeatureFlag.swift +++ b/Tests/FeatherQuillTests/MockFeatureFlag.swift @@ -36,5 +36,9 @@ internal static let initialValue: Int = .random(in: 1_000 ... 9_999) internal static let probability: Double = .random(in: 0 ..< 1) + + internal static func audienceCallback(_: AudienceType) async -> Bool { + true + } } #endif From 726b68864fc12ae09de87185781a22c15a3d475e Mon Sep 17 00:00:00 2001 From: leogdion Date: Fri, 10 May 2024 14:31:52 -0400 Subject: [PATCH 21/27] Update .swiftlint.yml --- .swiftlint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 30ba9c5..9684b78 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -41,7 +41,7 @@ opt_in_rules: - legacy_random - literal_expression_end_indentation - lower_acl_than_parent -# - missing_docs + - missing_docs - modifier_order - multiline_arguments - multiline_arguments_brackets @@ -128,4 +128,4 @@ fatal_error_message: disabled_rules: - nesting - implicit_getter - - switch_case_alignment \ No newline at end of file + - switch_case_alignment From 481c081634596aeec848fe163131dd5a492a91af Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 10 May 2024 16:39:47 -0400 Subject: [PATCH 22/27] fixing other issues --- Mintfile | 3 ++- Scripts/lint.sh | 1 + Sources/FeatherQuill/FeatureValue.swift | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mintfile b/Mintfile index c1dc548..44e3750 100644 --- a/Mintfile +++ b/Mintfile @@ -1,2 +1,3 @@ nicklockwood/SwiftFormat@0.53.5 -realm/SwiftLint@0.54.0 \ No newline at end of file +realm/SwiftLint@0.54.0 +peripheryapp/periphery@2.18.0 \ No newline at end of file diff --git a/Scripts/lint.sh b/Scripts/lint.sh index 31c3fa9..d30285e 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -38,6 +38,7 @@ if [ -z "$CI" ]; then $MINT_RUN swiftlint --fix fi +$MINT_RUN periphery scan $MINT_RUN swiftformat --lint $SWIFTFORMAT_OPTIONS . $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS diff --git a/Sources/FeatherQuill/FeatureValue.swift b/Sources/FeatherQuill/FeatureValue.swift index e948f89..1b554c2 100644 --- a/Sources/FeatherQuill/FeatureValue.swift +++ b/Sources/FeatherQuill/FeatureValue.swift @@ -36,7 +36,6 @@ public class FeatureValue { private let userDefaults: UserDefaults private let key: String - private let defaultValue: ValueType private let fullKey: String public var bindingValue: Binding { .init { @@ -68,7 +67,6 @@ ) { self.userDefaults = userDefaults self.key = key - self.defaultValue = defaultValue let initialValue: ValueType let fullKey = [ FeatureFlags.rootKey, self.key, FeatureFlags.valueKey From cc2bd8b141f257d13006b8a59b2a564c914c71f3 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 10 May 2024 18:10:13 -0400 Subject: [PATCH 23/27] adding public documentation --- Sources/FeatherQuill/Feature.swift | 9 ++++----- Sources/FeatherQuill/FeatureFlag.swift | 4 ++++ Sources/FeatherQuill/FeatureValue.swift | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Sources/FeatherQuill/Feature.swift b/Sources/FeatherQuill/Feature.swift index 9064527..c317278 100644 --- a/Sources/FeatherQuill/Feature.swift +++ b/Sources/FeatherQuill/Feature.swift @@ -32,22 +32,21 @@ import SwiftUI @Observable + /// Set of values for the Feature. public class Feature { private let featureValue: FeatureValue private let availability: FeatureAvailability + /// Binding value to use for SwiftUI Views. public var bindingValue: Binding { featureValue.bindingValue } + /// Value of the Feature. public var value: ValueType { featureValue.value } - public var isAvailable: Bool { - availability.value - } - fileprivate init( value: FeatureValue, availability: FeatureAvailability @@ -58,7 +57,7 @@ } extension Feature { - public convenience init( + internal convenience init( key: String, defaultValue: ValueType, userType: UserTypeValue, diff --git a/Sources/FeatherQuill/FeatureFlag.swift b/Sources/FeatherQuill/FeatureFlag.swift index a772eb7..4905b4a 100644 --- a/Sources/FeatherQuill/FeatureFlag.swift +++ b/Sources/FeatherQuill/FeatureFlag.swift @@ -69,18 +69,22 @@ "\(Self.self)" } + /// Matching user for ``FeatureFlag public static var audience: UserTypeValue { .default } + /// Behavior options on how to handle changes. public static var options: AvailabilityOptions { .default } + /// The key the ``FeatureFlag``. public static var key: String { FeatureFlagSuffixes.key(from: typeName) } + /// The default value for the environment key. public static var defaultValue: FeatherQuill.Feature { .init( key: key, diff --git a/Sources/FeatherQuill/FeatureValue.swift b/Sources/FeatherQuill/FeatureValue.swift index 1b554c2..65f8e23 100644 --- a/Sources/FeatherQuill/FeatureValue.swift +++ b/Sources/FeatherQuill/FeatureValue.swift @@ -33,11 +33,11 @@ import SwiftUI @Observable - public class FeatureValue { + internal class FeatureValue { private let userDefaults: UserDefaults private let key: String private let fullKey: String - public var bindingValue: Binding { + internal var bindingValue: Binding { .init { self._storedValue } set: { value in @@ -45,7 +45,7 @@ } } - public var value: ValueType { + internal var value: ValueType { let value = userDefaults.value(forKey: fullKey) as? ValueType assert(value != nil) return value ?? _storedValue From 86df90cee301f7b96b2ffd761dde995439587b26 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sat, 11 May 2024 21:06:59 -0400 Subject: [PATCH 24/27] refactoring audience setup --- Sources/FeatherQuill/FeatureFlag.swift | 4 ++-- Sources/FeatherQuill/UserType.swift | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/FeatherQuill/FeatureFlag.swift b/Sources/FeatherQuill/FeatureFlag.swift index 4905b4a..87830c4 100644 --- a/Sources/FeatherQuill/FeatureFlag.swift +++ b/Sources/FeatherQuill/FeatureFlag.swift @@ -61,7 +61,7 @@ static var options: AvailabilityOptions { get } @Sendable - static func audienceCallback(_ userType: UserTypeValue) async -> Bool + static func evaluateUser(_ userType: UserTypeValue) async -> Bool } extension FeatureFlag { @@ -91,7 +91,7 @@ defaultValue: initialValue, userType: audience, probability: probability, - audienceCallback + evaluateUser ) } } diff --git a/Sources/FeatherQuill/UserType.swift b/Sources/FeatherQuill/UserType.swift index 5a84520..c578f7b 100644 --- a/Sources/FeatherQuill/UserType.swift +++ b/Sources/FeatherQuill/UserType.swift @@ -29,6 +29,4 @@ public protocol UserType: OptionSet, Sendable where Self.RawValue: BinaryInteger { static var `default`: Self { get } - @available(*, deprecated) - static func includes(_ userType: Self) -> Bool } From 6e44e538a534d95515818faf002ed4cee2dcca13 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sat, 11 May 2024 21:07:33 -0400 Subject: [PATCH 25/27] saving readme work --- README.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dbc7eee..78104d8 100644 --- a/README.md +++ b/README.md @@ -1 +1,135 @@ -# FeatherQuill \ No newline at end of file +# FeatherQuill + +Easily rollout your new features to segments of your audience. + +[![SwiftPM](https://img.shields.io/badge/SPM-Linux%20%7C%20iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS-success?logo=swift)](https://swift.org) +[![Twitter](https://img.shields.io/badge/twitter-@brightdigit-blue.svg?style=flat)](http://twitter.com/brightdigit) +![GitHub](https://img.shields.io/github/license/brightdigit/FeatherQuill) +![GitHub issues](https://img.shields.io/github/issues/brightdigit/FeatherQuill) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/brightdigit/FeatherQuill/FeatherQuill.yml?label=actions&logo=github&?branch=main) + +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FFeatherQuill%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/brightdigit/FeatherQuill) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FFeatherQuill%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/brightdigit/FeatherQuill) + +[![Codecov](https://img.shields.io/codecov/c/github/brightdigit/FeatherQuill)](https://codecov.io/gh/brightdigit/FeatherQuill) +[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/brightdigit/FeatherQuill)](https://www.codefactor.io/repository/github/brightdigit/FeatherQuill) +[![codebeat badge](https://codebeat.co/badges/94a8313d-2215-4ef6-8690-ab7b3e06369c)](https://codebeat.co/projects/github-com-brightdigit-mistkit-main) +[![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/brightdigit/FeatherQuill)](https://codeclimate.com/github/brightdigit/FeatherQuill) +[![Code Climate technical debt](https://img.shields.io/codeclimate/tech-debt/brightdigit/FeatherQuill?label=debt)](https://codeclimate.com/github/brightdigit/FeatherQuill) +[![Code Climate issues](https://img.shields.io/codeclimate/issues/brightdigit/FeatherQuill)](https://codeclimate.com/github/brightdigit/FeatherQuill) +[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) + +## Features + +FeatherQuill is a Swift package that provides a mechanism for implementing offline feature flags in your application. Feature flags allow you to enable or disable features for different users or segments of your user base without requiring a server-side update. This can be useful for A/B testing, rollout strategies, and more. + +* **Offline Support:** Feature flags are stored locally on the device, so they can be used even when the device is not connected to the internet. +* **Audience Targeting:** You can target feature flags to specific users or segments of users based on criteria such as user ID or device type. +* **Metrics Collection:** FeatherQuill can collect metrics on how feature flags are being used, which can be helpful for evaluating the effectiveness of your A/B tests or other rollout strategies. + +## Requirements + +**Apple Platforms** + +- Xcode 15.3 or later +- Swift 5.10 or later +- iOS 17 / watchOS 10 / tvOS 17 / visionOS 1 / macCatalyst 17 / macOS 14 or later deployment targets + +**Linux** + +- Ubuntu 20.04 or later +- Swift 5.10 or later + +## Installation + +To add the FeatherQuill package to your Xcode project, select File > Swift Packages > Add Package Dependency and enter the repository URL. + +Using Swift Package Manager add the repository url: + +```swift +dependencies: [ + .package(url: "https://github.com/brightdigit/FeatherQuill", from: "1.0.0-alpha.1") +] +``` + +## Usage + +```swift +import FeatherQuill + +// Initialize the client with your bundle identifier as the domain +let plausible = Plausible(domain: "com.example.yourApp") + +// Define an event +let event = Event(url: "app://localhost/login") + +// Send the event +plausible.send(event: event) +``` + +### `Plausible` Client + +`Plausible` is a client for interacting with the Plausible API. It is initialized with a domain, which is typically your app's bundle identifier. The `Plausible` client is used to send events to the Plausible API for tracking and analysis. + +To construct a `Plausible` instance, you need to provide a domain. The domain is a string that identifies your application, typically the bundle identifier of your app. + +```swift +let plausible = Plausible(domain: "com.example.yourApp") +``` + +By default `Plausible` uses a `URLSessionTransport`, however you can use alternatives such as AsyncClient. + +### Sending an `Event` + +`Event` represents an event in your system. An event has a name, and optionally, a domain, URL, referrer, custom properties (`props`), and revenue information. You can create an `Event` instance and send it using the `Plausible` client. + +To construct an `Event`, you need to provide at least a name. The name is a string that identifies the event you want to track. Optionally, you can also provide: + +- **`name`** string that represents the name of the event. _Default_ is **pageview**. +- **`url`** string that represents the URL where the event occurred. For an app you may wish to use a app url such as `app://localhost/login`. +- `domain` _optional_ string that identifies the domain in which the event occurred. Overrides whatever was set in the `Plausible` instance. +- `referrer` _optional_ string that represents the URL of the referrer +- `props` _optional_ dictionary of custom properties associated with the event. +- `revenue` _optional_ `Revenue` instance that represents the revenue data associated with the event + +```swift +let event = Event + name: "eventName", + domain: "domain", + url: "url", + referrer: "referrer", + props: ["key": "value"], + revenue: Revenue( + currencyCode: "USD", + amount: 100 + ) +) +``` + +FeatherQuill provides two ways to send events to the Plausible API: + +#### Asynchronous Throwing Method + +This method sends an event to the Plausible API and throws an error if the operation fails. This is useful when you want to handle errors in your own way. Here's an example: + +```swift +do { + try await plausible.postEvent(event) +} catch { + print("Failed to post event: \(error)") +} +``` + +#### Synchronous Method + +This method sends an event to the Plausible API in the background and ignores any errors that occur. This is useful when you don't need to handle errors and want to fire-and-forget the event. Here's an example: + +```swift +plausible.postEvent(event) +``` + +In both cases, `event` is an instance of `Event` that you want to send to the Plausible API. + +## License + +FeatherQuill is available under the MIT license. See the [LICENSE](LICENSE) file for more info. \ No newline at end of file From 0d518fcaf33a5b665409022d87f3fd4ec8891bd4 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sun, 12 May 2024 17:40:21 -0400 Subject: [PATCH 26/27] fixing availability --- Sources/FeatherQuill/Feature.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/FeatherQuill/Feature.swift b/Sources/FeatherQuill/Feature.swift index c317278..4647551 100644 --- a/Sources/FeatherQuill/Feature.swift +++ b/Sources/FeatherQuill/Feature.swift @@ -47,6 +47,11 @@ featureValue.value } + /// Whether the feature is available to the user + public var isAvailable: Bool { + availability.value + } + fileprivate init( value: FeatureValue, availability: FeatureAvailability From 71ec4081a1e52e4f7041c7d64b59a63949ed41c6 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sun, 12 May 2024 20:42:52 -0400 Subject: [PATCH 27/27] fixing new refactor --- Demo/FeatureFlagsApp/ContentView.swift | 2 +- .../Sources/FeatureFlagsExample/AudienceType.swift | 9 --------- .../Sources/FeatureFlagsExample/NewDesignFeature.swift | 9 +++++++++ Sources/FeatherQuill/Feature.swift | 2 +- Tests/FeatherQuillTests/MockFeatureFlag.swift | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Demo/FeatureFlagsApp/ContentView.swift b/Demo/FeatureFlagsApp/ContentView.swift index 0587a63..1293ef8 100644 --- a/Demo/FeatureFlagsApp/ContentView.swift +++ b/Demo/FeatureFlagsApp/ContentView.swift @@ -39,7 +39,7 @@ struct ContentView: View { .foregroundStyle(.tint) Text("Hello, world!") - Toggle("Is Enabled", isOn: newDesign.value) + Toggle("Is Enabled", isOn: newDesign.bindingValue) .disabled(!newDesign.isAvailable) .opacity(newDesign.isAvailable ? 1.0 : 0.5) } diff --git a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift index c94532f..0c6aa39 100644 --- a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift +++ b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/AudienceType.swift @@ -35,15 +35,6 @@ public struct AudienceType: UserType { self.rawValue = rawValue } - public static func includes(_ value: AudienceType) -> Bool { - guard value.rawValue > 0 else { - return false - } - let value: Bool = .random() - print("User Matches: \(value)") - return value - } - public var rawValue: Int public typealias RawValue = Int diff --git a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift index 1890ba6..813b089 100644 --- a/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift +++ b/Demo/FeatureFlagsExample/Sources/FeatureFlagsExample/NewDesignFeature.swift @@ -36,6 +36,15 @@ struct NewDesignFeature: FeatureFlag { static let probability: Double = 0.5 static let initialValue = false + + static func evaluateUser(_ userType: AudienceType) async -> Bool { + guard userType.rawValue > 0 else { + return false + } + let value: Bool = .random() + print("User Matches: \(value)") + return value + } } extension EnvironmentValues { diff --git a/Sources/FeatherQuill/Feature.swift b/Sources/FeatherQuill/Feature.swift index 4647551..47c3b1d 100644 --- a/Sources/FeatherQuill/Feature.swift +++ b/Sources/FeatherQuill/Feature.swift @@ -31,8 +31,8 @@ import Observation import SwiftUI - @Observable /// Set of values for the Feature. + @Observable public class Feature { private let featureValue: FeatureValue private let availability: FeatureAvailability diff --git a/Tests/FeatherQuillTests/MockFeatureFlag.swift b/Tests/FeatherQuillTests/MockFeatureFlag.swift index 2fe1321..74aac17 100644 --- a/Tests/FeatherQuillTests/MockFeatureFlag.swift +++ b/Tests/FeatherQuillTests/MockFeatureFlag.swift @@ -37,7 +37,7 @@ internal static let probability: Double = .random(in: 0 ..< 1) - internal static func audienceCallback(_: AudienceType) async -> Bool { + internal static func evaluateUser(_: AudienceType) async -> Bool { true } }