├── .xcode-version
├── .ruby-version
├── TweakAccessorGenerator
├── img
└── just_tweak_banner.png
├── Example
├── JustTweak
│ ├── CodeGeneration
│ │ └── config.json
│ ├── Accessors
│ │ ├── GeneratedConfigurationAccessor+ExampleProtocol.swift
│ │ ├── GeneratedTweakAccessor+Constants.swift
│ │ ├── GeneratedTweakAccessor.swift
│ │ └── TweakAccessor.swift
│ ├── Protocols
│ │ └── ExampleProtocol.swift
│ ├── Assets
│ │ ├── Images.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── GoogleService-Info.plist
│ │ └── ExampleOptimizelyDatafile.json
│ ├── TweakProviders
│ │ ├── LocalTweaks_TopPriority_example.json
│ │ ├── LocalTweaks_example.json
│ │ ├── FirebaseTweakProvider.swift
│ │ └── OptimizelyTweakProvider.swift
│ ├── Info.plist
│ ├── UI
│ │ └── Base.lproj
│ │ │ └── LaunchScreen.xib
│ └── Code
│ │ ├── AppDelegate.swift
│ │ └── ViewController.swift
├── JustTweak.xcodeproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── JustTweak-Example.xcscheme
├── JustTweak.xcworkspace
│ ├── xcshareddata
│ │ ├── WorkspaceSettings.xcsettings
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── contents.xcworkspacedata
├── Podfile.lock
├── Tests
│ ├── UI
│ │ ├── BooleanTweakTableViewCellTests.swift
│ │ ├── TextTweakTableViewCellTests.swift
│ │ └── TweakViewControllerTests.swift
│ ├── LocalTweaks_test_override.json
│ ├── TestHelpers
│ │ ├── TestHelpers.swift
│ │ └── CustomOperators.swift
│ ├── Info.plist
│ ├── UnitTests.xctestplan
│ ├── Core
│ │ ├── EphemeralTweakProviderTests.swift
│ │ ├── TweaksUtilitiesTests.swift
│ │ ├── TweakExtensionsTests.swift
│ │ ├── LocalTweakProviderTests.swift
│ │ ├── UserDefaultsTweakProviderTests.swift
│ │ ├── TweakManagerCacheTests.swift
│ │ ├── TweakManager+PresentationTests.swift
│ │ ├── TweakTests.swift
│ │ ├── PropertyWrappersTests.swift
│ │ └── TweakManagerTests.swift
│ └── LocalTweaks_test.json
├── Podfile
└── Shared
│ └── Symbols.swift
├── Gemfile
├── cli-tools
└── TweakAccessorGenerator
│ ├── TweakAccessorGenerator
│ ├── Assets
│ │ ├── TestConfiguration1
│ │ │ └── config.json
│ │ └── TestConfiguration2
│ │ │ └── config.json
│ ├── Core
│ │ ├── Types.swift
│ │ ├── TweakValue.swift
│ │ ├── Models.swift
│ │ ├── TweakLoader.swift
│ │ └── TweakAccessorCodeGenerator.swift
│ ├── Extensions
│ │ ├── Array+Duplicates.swift
│ │ ├── NSNumber+ValueTypes.swift
│ │ └── String+Casing.swift
│ └── main.swift
│ ├── TweakAccessorGenerator.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── TweakAccessorGenerator.xcscheme
│ │ └── TweakAccessorGenerator-Release.xcscheme
│ └── TweakAccessorGeneratorTests
│ ├── Assets
│ ├── InvalidTweaks_InvalidJSON.json
│ ├── InvalidTweaks_MissingValues.json
│ ├── InvalidTweaks_DuplicateGeneratedPropertyName.json
│ ├── GeneratedTweakAccessor+ConstantsContent
│ ├── ValidTweaks_TopPriority.json
│ ├── ValidTweaks_LowPriority.json
│ ├── Tweaks.json
│ └── GeneratedTweakAccessorContent
│ ├── Suites
│ ├── Array+DuplicatesTests.swift
│ ├── NSNumber+ValueTypesTests.swift
│ ├── String+CasingTests.swift
│ ├── TweakAccessorCodeGeneratorTests.swift
│ └── TweakLoaderTests.swift
│ ├── Info.plist
│ └── UnitTests.xctestplan
├── Framework
└── Sources
│ ├── Resources
│ └── Localizations.bundle
│ │ └── en.lproj
│ │ └── Localizable.strings
│ ├── Extensions
│ ├── Bundle+JustTweak.swift
│ └── TweakExtensions.swift
│ ├── UI
│ ├── Cells
│ │ ├── NumericTweakTableViewCell.swift
│ │ ├── BooleanTweakTableViewCell.swift
│ │ └── TextTweakTableViewCell.swift
│ └── TweakManager+Presentation.swift
│ ├── Errors
│ └── TweakError.swift
│ ├── Protocols
│ └── TweakProvider.swift
│ ├── DTOs
│ ├── TweakValue.swift
│ └── Tweak.swift
│ ├── TweakProviders
│ ├── EphemeralTweakProvider.swift
│ ├── UserDefaultsTweakProvider.swift
│ └── LocalTweakProvider.swift
│ ├── Utilities
│ └── PropertyWrappers.swift
│ └── TweakManager.swift
├── Package.swift
├── .github
├── workflows
│ ├── publish-to-trunk-workflow.yml
│ └── pull-request-workflow.yml
└── PULL_REQUEST_TEMPLATE.md
├── fastlane
├── README.md
└── Fastfile
├── .gitignore
├── JustTweak.podspec
└── Gemfile.lock
/.xcode-version:
--------------------------------------------------------------------------------
1 | 10.2
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.0.2
2 |
3 |
--------------------------------------------------------------------------------
/TweakAccessorGenerator:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justeat/JustTweak/HEAD/TweakAccessorGenerator
--------------------------------------------------------------------------------
/img/just_tweak_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justeat/JustTweak/HEAD/img/just_tweak_banner.png
--------------------------------------------------------------------------------
/Example/JustTweak/CodeGeneration/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "accessorName": "GeneratedTweakAccessor"
3 | }
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | ruby '3.0.2'
2 |
3 | source 'https://rubygems.org'
4 |
5 | gem 'cocoapods', '~> 1.10.2'
6 | gem 'fastlane', '~> 2.191.0'
7 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration1/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "accessorName": "GeneratedTweakAccessor"
3 | }
4 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "accessorName": "GeneratedTweakAccessor"
3 | }
4 |
--------------------------------------------------------------------------------
/Example/JustTweak.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Framework/Sources/Resources/Localizations.bundle/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | "just_tweak_configurations_vc_title" = "Edit Configuration";
2 | "just_tweak_unnamed_tweaks_group_title" = "Other";
3 | "just_tweak_search_bar_placeholder_text" = "Search Tweaks";
4 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/JustTweak/Accessors/GeneratedConfigurationAccessor+ExampleProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeneratedConfigurationAccessor+ExampleProtocol.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | extension GeneratedTweakAccessor: ExampleProtocol { }
9 |
--------------------------------------------------------------------------------
/Example/JustTweak.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/JustTweak.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Framework/Sources/Extensions/Bundle+JustTweak.swift:
--------------------------------------------------------------------------------
1 | // Bundle+JustTweak.swift
2 |
3 | import Foundation
4 |
5 | public extension Bundle {
6 | class var justTweak: Bundle {
7 | #if SWIFT_PACKAGE
8 | Bundle.module
9 | #else
10 | Bundle(identifier: "org.cocoapods.JustTweak")!
11 | #endif
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Example/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - JustTweak (10.0.3)
3 |
4 | DEPENDENCIES:
5 | - JustTweak (from `../`)
6 |
7 | EXTERNAL SOURCES:
8 | JustTweak:
9 | :path: "../"
10 |
11 | SPEC CHECKSUMS:
12 | JustTweak: 57e7c41add4d1ec30d3b18d50be2403125f47e29
13 |
14 | PODFILE CHECKSUM: 60571fcd23f115b554ccbb8c325e342cef52d624
15 |
16 | COCOAPODS: 1.10.2
17 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Framework/Sources/UI/Cells/NumericTweakTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumericTweakTableViewCell
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import UIKit
7 |
8 | class NumericTweakTableViewCell: TextTweakTableViewCell {
9 |
10 | override var keyboardType: UIKeyboardType {
11 | get {
12 | return .numberPad
13 | }
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_InvalidJSON.json:
--------------------------------------------------------------------------------
1 | {
2 | "general": {
3 | "answer_to_the_universe": {
4 | "Title": "Definitive answer"
5 | "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything"
6 | "Group": "General"
7 | "Value": 42
8 | "GeneratedPropertyName": "definitiveAnswer"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Core/Types.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Types.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | typealias TweaksFormat = [FeatureKey: FeatureFormat]
9 | typealias FeatureFormat = [VariableKey: TweakFormat]
10 | typealias TweakFormat = [String: Any]
11 |
12 | typealias FeatureKey = String
13 | typealias VariableKey = String
14 |
--------------------------------------------------------------------------------
/Example/JustTweak.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakValue.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | public protocol TweakValue: CustomStringConvertible {}
9 |
10 | extension Bool: TweakValue {}
11 | extension Int: TweakValue {}
12 | extension Float: TweakValue {}
13 | extension Double: TweakValue {}
14 | extension String: TweakValue {}
15 |
--------------------------------------------------------------------------------
/Framework/Sources/Errors/TweakError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakError.swift
3 | // Copyright (c) 2022 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | public enum TweakError: String, Error {
9 | case notFound = "Feature or variable is not found"
10 | case notSupported = "Variable type is not supported"
11 | case decryptionClosureNotProvided = "Value is encrypted but there's no decryption closure provided"
12 | }
13 |
--------------------------------------------------------------------------------
/Example/JustTweak.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "swift-argument-parser",
6 | "repositoryURL": "https://github.com/apple/swift-argument-parser.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca",
10 | "version": "0.5.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "swift-argument-parser",
6 | "repositoryURL": "https://github.com/apple/swift-argument-parser.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca",
10 | "version": "0.5.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Core/Models.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Models.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | struct Tweak: Equatable {
9 | let feature: String
10 | let variable: String
11 | let title: String
12 | let description: String?
13 | let group: String
14 | let valueType: String
15 | let propertyName: String?
16 | }
17 |
18 | struct Configuration: Decodable {
19 | let accessorName: String
20 | }
21 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/Array+Duplicates.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+Duplicates.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | extension Array where Element: Hashable {
9 |
10 | var duplicates: Array {
11 | let groups = Dictionary(grouping: self, by: { $0 })
12 | let duplicateGroups = groups.filter { $1.count > 1 }
13 | let duplicates = Array(duplicateGroups.keys)
14 | return duplicates
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "JustTweak",
7 | defaultLocalization: "en",
8 | platforms: [
9 | .iOS(.v11)
10 | ],
11 | products: [
12 | .library(
13 | name: "JustTweak",
14 | targets: ["JustTweak"]),
15 | ],
16 | targets: [
17 | .target(
18 | name: "JustTweak",
19 | path: "Framework/Sources",
20 | resources: [.process("Resources")]
21 | )
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/Example/JustTweak/Protocols/ExampleProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeneratedConfigurationAccessor+ExampleProtocol.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | protocol ExampleProtocol {
9 | var meaningOfLife: Int { get }
10 | var shouldShowAlert: Bool { get }
11 | var isTapGestureToChangeColorEnabled: Bool { get }
12 | var canShowGreenView: Bool { get }
13 | var canShowRedView: Bool { get }
14 | var canShowYellowView: Bool { get }
15 | var labelText: String { get }
16 | var redViewAlpha: Double { get }
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/publish-to-trunk-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Publish to Trunk
2 | on:
3 | push:
4 | tags:
5 | - '*'
6 | jobs:
7 | build:
8 | runs-on: macOS-latest
9 | steps:
10 | - uses: actions/checkout@v1
11 | - name: Install Cocoapods
12 | run: gem install cocoapods
13 | - name: Deploy to Cocoapods
14 | run: |
15 | set -eo pipefail
16 | export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)
17 | pod lib lint --allow-warnings
18 | pod trunk push --allow-warnings
19 | env:
20 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
21 |
--------------------------------------------------------------------------------
/Example/Tests/UI/BooleanTweakTableViewCellTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BooleanTweakTableViewCellTests.swift
3 | // Copyright (c) 2019 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | @testable import JustTweak
8 |
9 | class BooleanTweakTableViewCellTests: XCTestCase {
10 |
11 | func testInformsDelegateOfValueChanges() {
12 | let mockDelegate = MockTweakCellDelegate()
13 | let cell = BooleanTweakTableViewCell()
14 | cell.delegate = mockDelegate
15 | cell.switchControl.sendActions(for: .valueChanged)
16 | XCTAssertTrue(mockDelegate.didCallDelegate)
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/Example/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://cdn.cocoapods.org/'
2 |
3 | platform :ios, '11.0'
4 | use_frameworks!
5 |
6 | target 'JustTweak_Example' do
7 | pod 'JustTweak', :path => '../'
8 | script_phase :name => 'TweakAccessorGenerator',
9 | :script => '$SRCROOT/../TweakAccessorGenerator \
10 | -l $SRCROOT/JustTweak/TweakProviders/LocalTweaks_example.json \
11 | -o $SRCROOT/JustTweak/Accessors/ \
12 | -c $SRCROOT/JustTweak/CodeGeneration/',
13 | :execution_position => :before_compile
14 |
15 | # pod 'Firebase/RemoteConfig'
16 | # pod 'OptimizelySDKiOS'
17 |
18 | target 'JustTweak_Tests' do
19 | inherit! :search_paths
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/Example/Tests/LocalTweaks_test_override.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui_customization": {
3 | "display_blue_view": {
4 | "Title": "Display Blue View",
5 | "Group": "UI Customization",
6 | "Value": true
7 | },
8 | "display_yellow_view": {
9 | "Title": "Display Yellow View",
10 | "Group": "UI Customization",
11 | "Value": true
12 | },
13 | "label_text": {
14 | "Title": "Label Text",
15 | "Group": "UI Customization",
16 | "Value": "Overridden value"
17 | }
18 | },
19 | "general": {
20 | "tap_to_change_color_enabled": {
21 | "Title": "Tap to change views color",
22 | "Group": "General",
23 | "Value": false
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/NSNumber+ValueTypes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSNumber+ValueTypes.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | public extension NSNumber {
9 |
10 | var tweakType: String {
11 | let encoding = String(cString: self.objCType)
12 | switch encoding {
13 | case "d":
14 | return "Double"
15 |
16 | case "q":
17 | return "Int"
18 |
19 | case "c":
20 | return "Bool"
21 |
22 | default:
23 | assert(false, "Unsupported objCType for NSNumber \(self)")
24 | return ""
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Example/Tests/TestHelpers/TestHelpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestHelpers.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | @testable import JustTweak
8 |
9 | class MockTweakCellDelegate: TweakViewControllerCellDelegate {
10 |
11 | private(set) var didCallDelegate: Bool = false
12 |
13 | func tweakConfigurationCellDidChangeValue(_ cell: TweakViewControllerCell) {
14 | didCallDelegate = true
15 | }
16 |
17 | }
18 |
19 | class MockPresentingViewController: UIViewController {
20 |
21 | private(set) var didCallDismissal: Bool = false
22 | override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
23 | didCallDismissal = true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Example/JustTweak/Assets/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/String+Casing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Casing.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | extension String {
9 |
10 | func camelCased(with separator: Character = "_") -> String {
11 | self
12 | .split(separator: separator)
13 | .enumerated()
14 | .map { $0.offset == 0 ? String($0.element).lowercasedFirstChar() : String($0.element).capitalisedFirstChar() }
15 | .joined()
16 | }
17 |
18 | func lowercasedFirstChar() -> String {
19 | prefix(1).lowercased() + self.dropFirst()
20 | }
21 |
22 | func capitalisedFirstChar() -> String {
23 | prefix(1).uppercased() + self.dropFirst()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Example/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/Array+DuplicatesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+DuplicatesTests.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import XCTest
7 |
8 | class Array_DuplicatesTests: XCTestCase {
9 |
10 | func test_noDuplicatesFound() {
11 | let arrayWithNoDuplicates = ["some", "array", "with", "no", "duplicates"]
12 | let expectedValue: [String] = []
13 | XCTAssertEqual(arrayWithNoDuplicates.duplicates, expectedValue)
14 | }
15 |
16 | func test_duplicatesFound() {
17 | let arrayWithDuplicates = ["some", "array", "with", "some", "duplicates", "here", "here", "and", "here"]
18 | let expectedValue = ["here", "some"]
19 | XCTAssertEqual(arrayWithDuplicates.duplicates.sorted(), expectedValue)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Example/Shared/Symbols.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Symbols.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import UIKit
7 | import JustTweak
8 |
9 | struct Features {
10 | static let uiCustomization = "ui_customization"
11 | static let general = "general"
12 | }
13 |
14 | struct Variables {
15 | static let redViewAlpha = "red_view_alpha_component"
16 | static let displayRedView = "display_red_view"
17 | static let displayGreenView = "display_green_view"
18 | static let displayYellowView = "display_yellow_view"
19 | static let tapToChangeViewColor = "tap_to_change_color_enabled"
20 | static let labelText = "label_text"
21 | static let greetOnAppDidBecomeActive = "greet_on_app_did_become_active"
22 | static let answerToTheUniverse = "answer_to_the_universe"
23 | static let meaningOfLife = "meaning_of_life"
24 | }
25 |
--------------------------------------------------------------------------------
/Example/Tests/UnitTests.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "7CD69B1F-AAC5-4B28-B048-0799F574A991",
5 | "name" : "Default",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "environmentVariableEntries" : [
13 | {
14 | "key" : "OS_ACTIVITY_MODE",
15 | "value" : "disable"
16 | }
17 | ],
18 | "targetForVariableExpansion" : {
19 | "containerPath" : "container:JustTweak.xcodeproj",
20 | "identifier" : "607FACCF1AFB9204008FA782",
21 | "name" : "JustTweak_Example"
22 | }
23 | },
24 | "testTargets" : [
25 | {
26 | "target" : {
27 | "containerPath" : "container:JustTweak.xcodeproj",
28 | "identifier" : "607FACE41AFB9204008FA782",
29 | "name" : "JustTweak_Tests"
30 | }
31 | }
32 | ],
33 | "version" : 1
34 | }
35 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_MissingValues.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui_customization": {
3 | "display_red_view": {
4 | "Description": "shows a red view in the main view controller",
5 | "Group": "UI Customization",
6 | "Value": false
7 | },
8 | "display_yellow_view": {
9 | "Title": "Display Yellow View",
10 | "Group": "UI Customization",
11 | "Value": false
12 | },
13 | "display_green_view": {
14 | "Title": "Display Green View",
15 | "Description": "shows a green view in the main view controller",
16 | "Value": true
17 | }
18 | },
19 | "general": {
20 | "answer_to_the_universe": {
21 | "Title": "Definitive answer",
22 | "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything",
23 | "Group": "General"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Example/Tests/TestHelpers/CustomOperators.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweaksUtilities.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | precedencegroup ComparisonPrecedence {
9 | associativity: left
10 | higherThan: LogicalConjunctionPrecedence
11 | }
12 |
13 | infix operator ?!: ComparisonPrecedence
14 | public func ?!(lhs: T?, rhs: T?) -> T? {
15 | return lhs == nil ? rhs : lhs
16 | }
17 | public func ?!(lhs: T?, rhs: T) -> T {
18 | return lhs == nil ? rhs : lhs!
19 | }
20 | public func ?!(lhs: Bool?, rhs: Bool?) -> Bool {
21 | return (lhs == nil ? rhs : lhs) ?? false
22 | }
23 |
24 | infix operator |||: ComparisonPrecedence
25 | public func |||(lhs: Bool?, rhs: Bool?) -> Bool {
26 | if let lhs = lhs, let rhs = rhs {
27 | return lhs || rhs
28 | }
29 | else if let lhs = lhs {
30 | return lhs
31 | }
32 | return rhs ?? false
33 | }
34 |
--------------------------------------------------------------------------------
/fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ================
3 | # Installation
4 |
5 | Make sure you have the latest version of the Xcode command line tools installed:
6 |
7 | ```
8 | xcode-select --install
9 | ```
10 |
11 | Install _fastlane_ using
12 | ```
13 | [sudo] gem install fastlane -NV
14 | ```
15 | or alternatively using `brew install fastlane`
16 |
17 | # Available Actions
18 | ## iOS
19 | ### ios unit_tests_just_tweak
20 | ```
21 | fastlane ios unit_tests_just_tweak
22 | ```
23 |
24 | ### ios unit_tests_tweak_accessor_generator
25 | ```
26 | fastlane ios unit_tests_tweak_accessor_generator
27 | ```
28 |
29 |
30 | ----
31 |
32 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
33 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
34 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
35 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_DuplicateGeneratedPropertyName.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui_customization": {
3 | "label_text": {
4 | "Title": "Label Text",
5 | "Description": "the title of the main label",
6 | "Group": "UI Customization",
7 | "Value": "Test value",
8 | "GeneratedPropertyName": "definitiveAnswer"
9 | }
10 | },
11 | "general": {
12 | "answer_to_the_universe": {
13 | "Title": "Definitive answer",
14 | "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything",
15 | "Group": "General",
16 | "Value": 42,
17 | "GeneratedPropertyName": "definitiveAnswer"
18 | },
19 | "tap_to_change_color_enabled": {
20 | "Title": "Tap to change views color",
21 | "Description": "change the colour of the main view when receiving a tap",
22 | "Group": "General",
23 | "Value": true
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Example/Tests/Core/EphemeralTweakProviderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EphemeralTweakProviderTests.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | @testable import JustTweak
8 |
9 | final class EphemeralTweakProviderTests: XCTestCase {
10 | private var ephemeralTweakProvider: NSDictionary!
11 |
12 | override func setUpWithError() throws {
13 | try super.setUpWithError()
14 | ephemeralTweakProvider = NSDictionary()
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | ephemeralTweakProvider = nil
19 | try super.tearDownWithError()
20 | }
21 |
22 | func testDefaultDecryptionClosure() {
23 | XCTAssertNil(ephemeralTweakProvider.decryptionClosure)
24 |
25 | ephemeralTweakProvider.decryptionClosure = { tweak in
26 | tweak.value
27 | }
28 |
29 | XCTAssertNil(ephemeralTweakProvider.decryptionClosure)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 | *.orig
22 |
23 | # Bundler
24 | .bundle
25 |
26 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
27 | # Carthage/Checkouts
28 |
29 | Carthage/Build
30 |
31 | # We recommend against adding the Pods directory to your .gitignore. However
32 | # you should judge for yourself, the pros and cons are mentioned at:
33 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
34 | #
35 | # Note: if you ignore the Pods directory, make sure to uncomment
36 | # `pod install` in .travis.yml
37 | #
38 | Pods/
39 | default.profraw
40 | fastlane/test_output/
41 | fastlane/report.xml
42 | derived_data/
43 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/GeneratedTweakAccessor+ConstantsContent:
--------------------------------------------------------------------------------
1 | // GeneratedTweakAccessorContent+Constants.swift
2 |
3 | /// Generated by TweakAccessorGenerator
4 |
5 | import Foundation
6 |
7 | extension GeneratedTweakAccessorContent {
8 |
9 | struct Features {
10 | static let general = "general"
11 | static let uiCustomization = "ui_customization"
12 | }
13 |
14 | struct Variables {
15 | static let answerToTheUniverse = "answer_to_the_universe"
16 | static let displayGreenView = "display_green_view"
17 | static let displayRedView = "display_red_view"
18 | static let displayYellowView = "display_yellow_view"
19 | static let greetOnAppDidBecomeActive = "greet_on_app_did_become_active"
20 | static let labelText = "label_text"
21 | static let redViewAlphaComponent = "red_view_alpha_component"
22 | static let tapToChangeColorEnabled = "tap_to_change_color_enabled"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Example/JustTweak/Accessors/GeneratedTweakAccessor+Constants.swift:
--------------------------------------------------------------------------------
1 | // GeneratedTweakAccessor+Constants.swift
2 |
3 | /// Generated by TweakAccessorGenerator
4 |
5 | import Foundation
6 |
7 | extension GeneratedTweakAccessor {
8 |
9 | struct Features {
10 | static let general = "general"
11 | static let uiCustomization = "ui_customization"
12 | }
13 |
14 | struct Variables {
15 | static let answerToTheUniverse = "answer_to_the_universe"
16 | static let displayGreenView = "display_green_view"
17 | static let displayRedView = "display_red_view"
18 | static let displayYellowView = "display_yellow_view"
19 | static let encryptedAnswerToTheUniverse = "encrypted_answer_to_the_universe"
20 | static let greetOnAppDidBecomeActive = "greet_on_app_did_become_active"
21 | static let labelText = "label_text"
22 | static let redViewAlphaComponent = "red_view_alpha_component"
23 | static let tapToChangeColorEnabled = "tap_to_change_color_enabled"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Framework/Sources/Protocols/TweakProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakProvider.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | public enum LogLevel: Int {
9 | case error, debug, verbose
10 | }
11 |
12 | public typealias LogClosure = (String, LogLevel) -> Void
13 |
14 | public protocol TweakProvider {
15 | var logClosure: LogClosure? { set get }
16 | func isFeatureEnabled(_ feature: String) -> Bool
17 | func tweakWith(feature: String, variable: String) throws -> Tweak
18 |
19 | var decryptionClosure: ((Tweak) -> TweakValue)? { get set }
20 | }
21 |
22 | public protocol MutableTweakProvider: TweakProvider {
23 | func set(_ value: TweakValue, feature: String, variable: String)
24 | func deleteValue(feature: String, variable: String)
25 | }
26 |
27 | public let TweakProviderDidChangeNotification = Notification.Name("TweakProviderDidChangeNotification")
28 | public let TweakProviderDidChangeNotificationTweakKey = "TweakProviderDidChangeNotificationTweakKey"
29 |
--------------------------------------------------------------------------------
/JustTweak.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'JustTweak'
3 | s.version = ENV['LIB_VERSION']
4 | s.summary = 'A framework for feature flagging, locally and remotely configure and A/B test iOS apps.'
5 | s.description = <<-DESC
6 | JustTweak is a framework for feature flagging, locally and remotely configure and A/B test iOS apps.
7 | DESC
8 |
9 | s.homepage = 'https://github.com/justeat/JustTweak'
10 | s.license = { :type => 'Apache 2.0', :file => 'LICENSE' }
11 | s.author = 'Just Eat Takeaway iOS Team'
12 | s.source = { :git => 'https://github.com/justeat/JustTweak.git', :tag => s.version.to_s }
13 |
14 | s.ios.deployment_target = '11.0'
15 | s.swift_version = '5.1'
16 |
17 | s.source_files = 'Framework/Sources/**/*.swift'
18 | s.resources = 'Framework/Sources/Resources/**/*'
19 |
20 | s.preserve_paths = [
21 | 'TweakAccessorGenerator',
22 | ]
23 |
24 | end
25 |
--------------------------------------------------------------------------------
/Example/JustTweak/TweakProviders/LocalTweaks_TopPriority_example.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui_customization": {
3 | "display_red_view": {
4 | "Title": "Display Red View",
5 | "Description": "shows a red view in the main view controller",
6 | "Group": "UI Customization",
7 | "Value": true
8 | },
9 | "display_yellow_view": {
10 | "Title": "Display Yellow View",
11 | "Description": "shows a yellow view in the main view controller",
12 | "Group": "UI Customization",
13 | "Value": true
14 | },
15 | "display_green_view": {
16 | "Title": "Display Green View",
17 | "Description": "shows a green view in the main view controller",
18 | "Group": "UI Customization",
19 | "Value": false
20 | }
21 | },
22 | "general": {
23 | "answer_to_the_universe": {
24 | "Title": "Definitive answer",
25 | "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything",
26 | "Group": "General",
27 | "Value": 42,
28 | "GeneratedPropertyName": "definitiveAnswer"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/ValidTweaks_TopPriority.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui_customization": {
3 | "display_red_view": {
4 | "Title": "Display Red View",
5 | "Description": "shows a red view in the main view controller",
6 | "Group": "UI Customization",
7 | "Value": true
8 | },
9 | "display_yellow_view": {
10 | "Title": "Display Yellow View",
11 | "Description": "shows a yellow view in the main view controller",
12 | "Group": "UI Customization",
13 | "Value": true
14 | },
15 | "display_green_view": {
16 | "Title": "Display Green View",
17 | "Description": "shows a green view in the main view controller",
18 | "Group": "UI Customization",
19 | "Value": false
20 | }
21 | },
22 | "general": {
23 | "answer_to_the_universe": {
24 | "Title": "Definitive answer",
25 | "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything",
26 | "Group": "General",
27 | "Value": 42,
28 | "GeneratedPropertyName": "definitiveAnswer"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Example/Tests/LocalTweaks_test.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui_customization": {
3 | "display_red_view": {
4 | "Title": "Display Red View",
5 | "Group": "UI Customization",
6 | "Value": true
7 | },
8 | "display_yellow_view": {
9 | "Title": "Display Yellow View",
10 | "Group": "UI Customization",
11 | "Value": false
12 | },
13 | "display_green_view": {
14 | "Title": "Display Green View",
15 | "Group": "UI Customization",
16 | "Value": true
17 | },
18 | "red_view_alpha_component": {
19 | "Title": "Red View Alpha Component",
20 | "Group": "UI Customization",
21 | "Value": 1.0
22 | },
23 | "label_text": {
24 | "Title": "Label Text",
25 | "Group": "UI Customization",
26 | "Value": "Test value"
27 | }
28 | },
29 | "general": {
30 | "greet_on_app_did_become_active": {
31 | "Title": "Greet on app launch",
32 | "Group": "General",
33 | "Value": false
34 | },
35 | "tap_to_change_color_enabled": {
36 | "Title": "Tap to change views color",
37 | "Group": "General",
38 | "Value": true
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Framework/Sources/DTOs/TweakValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakValue.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | public protocol TweakValue: CustomStringConvertible {}
9 |
10 | extension Bool: TweakValue {}
11 | extension Int: TweakValue {}
12 | extension Float: TweakValue {}
13 | extension Double: TweakValue {}
14 | extension String: TweakValue {}
15 |
16 | public extension TweakValue {
17 |
18 | var intValue: Int {
19 | return Int(doubleValue)
20 | }
21 |
22 | var floatValue: Float {
23 | return Float(doubleValue)
24 | }
25 |
26 | var doubleValue: Double {
27 | return Double(description) ?? 0.0
28 | }
29 |
30 | var boolValue: Bool {
31 | return self as? Bool ?? false
32 | }
33 |
34 | var stringValue: String? {
35 | return self as? String
36 | }
37 | }
38 |
39 | public func ==(lhs: TweakValue, rhs: TweakValue) -> Bool {
40 | if let lhs = lhs as? String, let rhs = rhs as? String {
41 | return lhs == rhs
42 | }
43 | return NSNumber(tweakValue: lhs) == NSNumber(tweakValue: rhs)
44 | }
45 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/NSNumber+ValueTypesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSNumber+ValueTypesTests.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import XCTest
8 |
9 | class NSNumber_ValueTypesTests: XCTestCase {
10 |
11 | func test_tweakType_for_NSNumber_with_Int_value() {
12 | let sut = NSNumber(value: 42)
13 | XCTAssertEqual(sut.tweakType, "Int")
14 | }
15 |
16 | func test_tweakType_for_NSNumber_with_IntegerLiteral_value() {
17 | let sut2 = NSNumber(integerLiteral: 42)
18 | XCTAssertEqual(sut2.tweakType, "Int")
19 | }
20 |
21 | func test_tweakType_for_NSNumber_with_Double_value() {
22 | let sut = NSNumber(value: 3.14)
23 | XCTAssertEqual(sut.tweakType, "Double")
24 | }
25 |
26 | func test_tweakType_for_NSNumber_with_FloatLiteral_value() {
27 | let sut = NSNumber(floatLiteral: 3.14)
28 | XCTAssertEqual(sut.tweakType, "Double")
29 | }
30 |
31 | func test_tweakType_for_NSNumber_with_Bool_value() {
32 | let sut = NSNumber(value: true)
33 | XCTAssertEqual(sut.tweakType, "Bool")
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/UnitTests.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "5ACF2D18-3EDB-4507-A1E4-C3ECF1D3D55F",
5 | "name" : "Default",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "commandLineArgumentEntries" : [
13 | {
14 | "argument" : "-l $SRCROOT\/..\/Example\/JustTweak\/TweakProviders\/LocalTweaks_example.json"
15 | },
16 | {
17 | "argument" : "-o $SRCROOT\/..\/Example\/JustTweak\/Accessors\/"
18 | },
19 | {
20 | "argument" : "-c $SRCROOT\/TweakAccessorGenerator\/Assets\/TestConfiguration1\/"
21 | },
22 | {
23 | "argument" : "--help",
24 | "enabled" : false
25 | }
26 | ],
27 | "targetForVariableExpansion" : {
28 | "containerPath" : "container:TweakAccessorGenerator.xcodeproj",
29 | "identifier" : "12273D162625D6BE00732559",
30 | "name" : "TweakAccessorGenerator"
31 | }
32 | },
33 | "testTargets" : [
34 | {
35 | "target" : {
36 | "containerPath" : "container:TweakAccessorGenerator.xcodeproj",
37 | "identifier" : "12BE96492626FA5000C1B6C3",
38 | "name" : "TweakAccessorGeneratorTests"
39 | }
40 | }
41 | ],
42 | "version" : 1
43 | }
44 |
--------------------------------------------------------------------------------
/Example/JustTweak/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Description
4 |
5 |
6 | ## Motivation and Context
7 |
8 |
9 |
10 | ## How Has This Been Tested?
11 |
12 |
13 |
14 |
15 | ## Screenshots (if appropriate):
16 |
17 | ## Types of changes
18 |
19 | - [ ] Bug fix (non-breaking change which fixes an issue)
20 | - [ ] New feature (non-breaking change which adds functionality)
21 | - [ ] Breaking change (fix or feature that would cause existing functionality to change)
22 |
23 | ## Checklist:
24 |
25 |
26 | - [ ] My code follows the code style of this project.
27 | - [ ] My change requires a change to the documentation.
28 | - [ ] I have updated the documentation accordingly.
29 |
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | default_platform :ios
2 |
3 | platform :ios do
4 |
5 | before_all do
6 | ensure_bundle_exec
7 | $derived_data_folder = "./derived_data"
8 | $workspace_filename = "Example/JustTweak.xcworkspace"
9 | $common_test_xcargs = "COMPILER_INDEX_STORE_ENABLE=NO"
10 | end
11 |
12 | lane :unit_tests_just_tweak do |parameters|
13 | UI.user_error! "Missing parameter 'device'" unless parameters.has_key?(:device)
14 | device = parameters[:device]
15 | run_tests(
16 | workspace: $workspace_filename,
17 | scheme: "JustTweak-Example",
18 | testplan: "UnitTests",
19 | device: device,
20 | code_coverage: true,
21 | result_bundle: true,
22 | concurrent_workers: 1,
23 | xcargs: $common_test_xcargs,
24 | derived_data_path: $derived_data_folder
25 | )
26 | end
27 |
28 | lane :unit_tests_tweak_accessor_generator do |parameters|
29 | run_tests(
30 | workspace: $workspace_filename,
31 | scheme: "TweakAccessorGenerator",
32 | testplan: "UnitTests",
33 | code_coverage: true,
34 | result_bundle: true,
35 | concurrent_workers: 1,
36 | xcargs: $common_test_xcargs,
37 | derived_data_path: $derived_data_folder
38 | )
39 | end
40 |
41 | end
42 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/String+CasingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+CasingTests.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import XCTest
7 |
8 | class String_CasingTests: XCTestCase {
9 |
10 | func test_camelCased_with_default_separator_1() {
11 | let sut = "Some-Property_Key_some-Value_some"
12 | let expectedValue = "some-PropertyKeySome-ValueSome"
13 | XCTAssertEqual(expectedValue, sut.camelCased())
14 | }
15 |
16 | func test_camelCased_with_default_separator_2() {
17 | let sut = "Some_PropertyKey"
18 | let expectedValue = "somePropertyKey"
19 | XCTAssertEqual(expectedValue, sut.camelCased())
20 | }
21 |
22 | func test_camelCased_with_custom_separator() {
23 | let sut = "Some-Property:Key:some-Value:Some"
24 | let expectedValue = "some-PropertyKeySome-ValueSome"
25 | XCTAssertEqual(expectedValue, sut.camelCased(with: ":"))
26 | }
27 |
28 | func test_lowercasedFirstChar() {
29 | let sut = "SomeName"
30 | let expectedValue = "someName"
31 | XCTAssertEqual(expectedValue, sut.lowercasedFirstChar())
32 | }
33 |
34 | func test_capitalisedFirstChar() {
35 | let sut = "someName"
36 | let expectedValue = "SomeName"
37 | XCTAssertEqual(expectedValue, sut.capitalisedFirstChar())
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/pull-request-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Pull Request Workflow
2 | on: [pull_request]
3 | jobs:
4 | run-tests:
5 | runs-on: macOS-latest
6 | timeout-minutes: 15
7 | steps:
8 | - name: Cancel previous jobs
9 | uses: styfle/cancel-workflow-action@0.6.0
10 | with:
11 | access_token: ${{ github.token }}
12 | - name: Git checkout
13 | uses: actions/checkout@v2.3.4
14 | with:
15 | fetch-depth: 0
16 | ref: ${{ github.ref }}
17 | - name: Setup Xcode
18 | uses: maxim-lobanov/setup-xcode@v1
19 | with:
20 | xcode-version: latest-stable
21 | - name: Setup ruby and bundler dependencies
22 | uses: ruby/setup-ruby@v1.81.0
23 | with:
24 | bundler-cache: true
25 | - name: Run pod install
26 | run: |
27 | set -eo pipefail
28 | export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)
29 | bundle exec pod install --project-directory=Example
30 | - name: Run tests (JustTweak)
31 | run: bundle exec fastlane unit_tests_just_tweak device:'iPhone 11'
32 | - name: Run tests (TweakAccessorGenerator)
33 | run: bundle exec fastlane unit_tests_tweak_accessor_generator
34 | - name: Validate lib
35 | run: |
36 | set -eo pipefail
37 | export LIB_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)
38 | bundle exec pod lib lint --allow-warnings
39 |
--------------------------------------------------------------------------------
/Framework/Sources/TweakProviders/EphemeralTweakProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EphemeralTweakProvider.swift
3 | // Copyright (c) 2018 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | extension NSDictionary: TweakProvider {
9 |
10 | public var logClosure: LogClosure? {
11 | get { return nil }
12 | set { }
13 | }
14 |
15 | public var decryptionClosure: ((Tweak) -> TweakValue)? {
16 | get {
17 | nil
18 | }
19 | set {}
20 | }
21 |
22 | public func isFeatureEnabled(_ feature: String) -> Bool {
23 | self[feature] as? Bool ?? false
24 | }
25 |
26 | public func tweakWith(feature: String, variable: String) throws -> Tweak {
27 | guard let storedValue = self[variable] else { throw TweakError.notFound }
28 | var value: TweakValue? = nil
29 | if let theValue = storedValue as? String {
30 | value = theValue
31 | }
32 | else if let theValue = storedValue as? NSNumber {
33 | value = theValue.tweakValue
34 | }
35 | guard let finalValue = value else { throw TweakError.notSupported }
36 | return Tweak(feature: feature, variable: variable, value: finalValue)
37 | }
38 | }
39 |
40 | extension NSMutableDictionary: MutableTweakProvider {
41 |
42 | public func set(_ value: TweakValue, feature: String, variable: String) {
43 | self[variable] = value
44 | }
45 |
46 | public func deleteValue(feature: String, variable: String) {
47 | removeObject(forKey: variable)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Framework/Sources/Extensions/TweakExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakExtensions.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | public extension String {
9 |
10 | var tweakValue: TweakValue {
11 | if let bool = Bool(self.lowercased()) {
12 | return bool
13 | }
14 | else if let doubleValue = Double(self) {
15 | return doubleValue
16 | }
17 | return self
18 | }
19 | }
20 |
21 | public extension NSNumber {
22 |
23 | var tweakValue: TweakValue {
24 | let encoding = String(cString: self.objCType)
25 | switch encoding {
26 | case "d":
27 | return self.doubleValue
28 |
29 | case "f":
30 | return self.floatValue
31 |
32 | case "c":
33 | return self.boolValue
34 |
35 | default:
36 | return self.intValue
37 | }
38 | }
39 |
40 | convenience init?(tweakValue: TweakValue) {
41 | if let tweakValue = tweakValue as? Bool {
42 | self.init(value: tweakValue as Bool)
43 | }
44 | else if let tweakValue = tweakValue as? Int {
45 | self.init(value: tweakValue as Int)
46 | }
47 | else if let tweakValue = tweakValue as? Float {
48 | self.init(value: tweakValue as Float)
49 | }
50 | else if let tweakValue = tweakValue as? Double {
51 | self.init(value: tweakValue as Double)
52 | }
53 | else {
54 | return nil
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Framework/Sources/UI/TweakManager+Presentation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakManager+Presentation.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | extension TweakManager {
9 |
10 | var displayableTweaks: [Tweak] {
11 | var tweaks = [String : Tweak]()
12 | for localTweakProvider in self.localTweakProviders.reversed() {
13 | for (feature, variables) in localTweakProvider.features {
14 | for variable in variables {
15 | if let tweak = try? tweakWith(feature: feature, variable: variable),
16 | let jsonTweak = try? localTweakProvider.tweakWith(feature: feature, variable: variable) {
17 | let aggregatedTweak = Tweak(feature: feature,
18 | variable: variable,
19 | value: tweak.value,
20 | title: jsonTweak.title,
21 | description: jsonTweak.desc,
22 | group: jsonTweak.group)
23 | let key = "\(feature)-\(variable)"
24 | tweaks[key] = aggregatedTweak
25 | }
26 | }
27 | }
28 | }
29 | return tweaks.values.sorted(by: { $0.displayTitle < $1.displayTitle })
30 | }
31 |
32 | private var localTweakProviders: [LocalTweakProvider] {
33 | return tweakProviders.filter { $0 is LocalTweakProvider } as! [LocalTweakProvider]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Example/JustTweak/Assets/GoogleService-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AD_UNIT_ID_FOR_BANNER_TEST
6 | <DOWNLOAD_GoogleService.plist_FROM_YOUR_APP_IN_FIREBASE_CONSOLE>
7 | AD_UNIT_ID_FOR_INTERSTITIAL_TEST
8 | <DOWNLOAD_GoogleService.plist_FROM_YOUR_APP_IN_FIREBASE_CONSOLE>
9 | CLIENT_ID
10 | <DOWNLOAD_GoogleService.plist_FROM_YOUR_APP_IN_FIREBASE_CONSOLE>
11 | REVERSED_CLIENT_ID
12 | <DOWNLOAD_GoogleService.plist_FROM_YOUR_APP_IN_FIREBASE_CONSOLE>
13 | API_KEY
14 | <DOWNLOAD_GoogleService.plist_FROM_YOUR_APP_IN_FIREBASE_CONSOLE>
15 | GCM_SENDER_ID
16 | <DOWNLOAD_GoogleService.plist_FROM_YOUR_APP_IN_FIREBASE_CONSOLE>
17 | PLIST_VERSION
18 | 1
19 | BUNDLE_ID
20 | com.justeat.JustTweak.Example
21 | PROJECT_ID
22 | just-tweak-example
23 | STORAGE_BUCKET
24 | just-tweak-example.appspot.com
25 | IS_ADS_ENABLED
26 |
27 | IS_ANALYTICS_ENABLED
28 |
29 | IS_APPINVITE_ENABLED
30 |
31 | IS_GCM_ENABLED
32 |
33 | IS_SIGNIN_ENABLED
34 |
35 | GOOGLE_APP_ID
36 | <DOWNLOAD_GoogleService.plist_FROM_YOUR_APP_IN_FIREBASE_CONSOLE>
37 | DATABASE_URL
38 | <DOWNLOAD_GoogleService.plist_FROM_YOUR_APP_IN_FIREBASE_CONSOLE>
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Framework/Sources/UI/Cells/BooleanTweakTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BooleanTweakTableViewCell
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import UIKit
7 |
8 | internal class BooleanTweakTableViewCell: UITableViewCell, TweakViewControllerCell {
9 |
10 | var feature: String?
11 | var variable: String?
12 |
13 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
14 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
15 | }
16 |
17 | required init?(coder aDecoder: NSCoder) {
18 | fatalError("init(coder:) has not been implemented")
19 | }
20 |
21 | var title: String? {
22 | get {
23 | return textLabel?.text
24 | }
25 | set {
26 | textLabel?.text = newValue
27 | }
28 | }
29 |
30 | var desc: String? {
31 | get {
32 | return detailTextLabel?.text
33 | }
34 | set {
35 | detailTextLabel?.text = newValue
36 | }
37 | }
38 |
39 | var value: TweakValue {
40 | get {
41 | return switchControl.isOn
42 | }
43 | set {
44 | switchControl.isOn = newValue.boolValue
45 | }
46 | }
47 | weak var delegate: TweakViewControllerCellDelegate?
48 |
49 | lazy var switchControl: UISwitch! = {
50 | let switchControl = UISwitch()
51 | switchControl.addTarget(self, action: #selector(didChangeTweakValue), for: .valueChanged)
52 | self.accessoryView = switchControl
53 | self.selectionStyle = .none
54 | return switchControl
55 | }()
56 |
57 | @objc func didChangeTweakValue() {
58 | delegate?.tweakConfigurationCellDidChangeValue(self)
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Example/Tests/UI/TextTweakTableViewCellTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextTweakTableViewCellTests.swift
3 | // Copyright (c) 2019 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | @testable import JustTweak
8 |
9 | class TextTweakTableViewCellTests: XCTestCase {
10 |
11 | func testInformsDelegateOfValueChanges() {
12 | let mockDelegate = MockTweakCellDelegate()
13 | let cell = TextTweakTableViewCell()
14 | cell.delegate = mockDelegate
15 | cell.textField.sendActions(for: .editingDidEnd)
16 | XCTAssertTrue(mockDelegate.didCallDelegate)
17 | }
18 |
19 | func testUpdatesTweakValueWhenTextChanges() {
20 | let cell = TextTweakTableViewCell()
21 | cell.value = "Old Value"
22 | cell.textField.text = "Some new value"
23 | cell.textField.sendActions(for: .editingChanged)
24 | XCTAssertTrue(cell.value == "Some new value")
25 | }
26 |
27 | func testUpdatesTweakValueWhenTextChangesToNil() {
28 | let cell = TextTweakTableViewCell()
29 | cell.value = "Old Value"
30 | cell.textField.text = nil
31 | cell.textField.sendActions(for: .editingChanged)
32 | XCTAssertTrue(cell.value == "")
33 | }
34 |
35 | func testTextFieldNeverGrowsMoreThanHalfTheSizeOfTheCell() {
36 | let cell = TextTweakTableViewCell(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
37 | cell.value = "Some extremely long string that wouldn't fit in 320 points"
38 | XCTAssertTrue(cell.textField.bounds.width <= 160)
39 | }
40 |
41 | func testTextFieldResignsFirstResponderOnReturn() {
42 | let cell = TextTweakTableViewCell(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
43 | // HACK to get the text field to become first responder
44 | UIApplication.shared.delegate?.window??.addSubview(cell)
45 | cell.textField.becomeFirstResponder()
46 | RunLoop.main.run(until: Date(timeIntervalSinceNow: 0))
47 | // HACKEND
48 | XCTAssertTrue(cell.textField.isFirstResponder)
49 | let _ = cell.textField.delegate?.textFieldShouldReturn?(cell.textField)
50 | XCTAssertFalse(cell.textField.isFirstResponder)
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/ValidTweaks_LowPriority.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui_customization": {
3 | "display_red_view": {
4 | "Title": "Display Red View",
5 | "Description": "shows a red view in the main view controller",
6 | "Group": "UI Customization",
7 | "Value": false,
8 | "GeneratedPropertyName": "canShowRedView"
9 | },
10 | "display_yellow_view": {
11 | "Title": "Display Yellow View",
12 | "Description": "shows a yellow view in the main view controller",
13 | "Group": "UI Customization",
14 | "Value": false,
15 | "GeneratedPropertyName": "canShowYellowView"
16 | },
17 | "display_green_view": {
18 | "Title": "Display Green View",
19 | "Description": "shows a green view in the main view controller",
20 | "Group": "UI Customization",
21 | "Value": true,
22 | "GeneratedPropertyName": "canShowGreenView"
23 | },
24 | "red_view_alpha_component": {
25 | "Title": "Red View Alpha Component",
26 | "Description": "defines the alpha level of the red view",
27 | "Group": "UI Customization",
28 | "Value": 1.0,
29 | "GeneratedPropertyName": "redViewAlpha"
30 | },
31 | "label_text": {
32 | "Title": "Label Text",
33 | "Description": "the title of the main label",
34 | "Group": "UI Customization",
35 | "Value": "Test value"
36 | }
37 | },
38 | "general": {
39 | "answer_to_the_universe": {
40 | "Title": "Definitive answer",
41 | "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything",
42 | "Group": "General",
43 | "Value": 42,
44 | "GeneratedPropertyName": "meaningOfLife"
45 | },
46 | "greet_on_app_did_become_active": {
47 | "Title": "Greet on app launch",
48 | "Description": "shows an alert on applicationDidBecomeActive",
49 | "Group": "General",
50 | "Value": false,
51 | "GeneratedPropertyName": "shouldShowAlert"
52 | },
53 | "tap_to_change_color_enabled": {
54 | "Title": "Tap to change views color",
55 | "Description": "change the colour of the main view when receiving a tap",
56 | "Group": "General",
57 | "Value": true,
58 | "GeneratedPropertyName": "isTapGestureToChangeColorEnabled"
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/Tweaks.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui_customization": {
3 | "display_red_view": {
4 | "Title": "Display Red View",
5 | "Description": "shows a red view in the main view controller",
6 | "Group": "UI Customization",
7 | "Value": false
8 | },
9 | "display_yellow_view": {
10 | "Title": "Display Yellow View",
11 | "Description": "shows a yellow view in the main view controller",
12 | "Group": "UI Customization",
13 | "Value": false
14 | },
15 | "display_green_view": {
16 | "Title": "Display Green View",
17 | "Description": "shows a green view in the main view controller",
18 | "Group": "UI Customization",
19 | "Value": true
20 | },
21 | "red_view_alpha_component": {
22 | "Title": "Red View Alpha Component",
23 | "Description": "defines the alpha level of the red view",
24 | "Group": "UI Customization",
25 | "Value": 1.0
26 | },
27 | "label_text": {
28 | "Title": "Label Text",
29 | "Description": "the title of the main label",
30 | "Group": "UI Customization",
31 | "Value": "Test value"
32 | }
33 | },
34 | "general": {
35 | "encrypted_answer_to_the_universe": {
36 | "Title": "Encrypted definitive answer",
37 | "Description": "Encrypted answer to the Ultimate Question of Life, the Universe, and Everything",
38 | "Group": "General",
39 | "Value": "24 ton yletinifeD",
40 | "GeneratedPropertyName": "definitiveAnswerEncrypted",
41 | "Encrypted": true
42 | },
43 | "answer_to_the_universe": {
44 | "Title": "Definitive answer",
45 | "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything",
46 | "Group": "General",
47 | "Value": 42,
48 | "GeneratedPropertyName": "definitiveAnswer"
49 | },
50 | "greet_on_app_did_become_active": {
51 | "Title": "Greet on app launch",
52 | "Description": "shows an alert on applicationDidBecomeActive",
53 | "Group": "General",
54 | "Value": false
55 | },
56 | "tap_to_change_color_enabled": {
57 | "Title": "Tap to change views color",
58 | "Description": "change the colour of the main view when receiving a tap",
59 | "Group": "General",
60 | "Value": true
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Example/Tests/Core/TweaksUtilitiesTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweaksUtilitiesTests.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | import JustTweak
8 |
9 | class TweaksUtilitiesTests: XCTestCase {
10 |
11 | func testExclusiveBinaryOperator_ReturnsFirstArgument_WhenFirstArgumentIsNotNil() {
12 | XCTAssertEqual("a" ?! "b", "a")
13 | }
14 |
15 | func testExclusiveBinaryOperator_ReturnsSecondArgument_WhenFirstArgumentIsNil() {
16 | XCTAssertEqual(nil ?! "b", "b")
17 | }
18 |
19 | func testExclusiveBinaryOperator_ReturnsTrue_WhenFirstArgumentIsNilAndSecondIsTrue() {
20 | XCTAssertTrue(nil ?! true)
21 | }
22 |
23 | func testExclusiveBinaryOperator_ReturnsFalse_WhenFirstArgumentIsFalseAndSecondIsTrue() {
24 | XCTAssertFalse(false ?! true)
25 | }
26 |
27 | func testExclusiveBinaryOperator_ReturnsNil_WhenBothArgumentsAreNil() {
28 | let a: String? = nil
29 | let b: String? = nil
30 | XCTAssertNil(a ?! b)
31 | }
32 |
33 | func testInclusingOrForBooleansOperator_ReturnsFalse_WhenFirstArgumentIsFalseAndSecondIsFalse() {
34 | XCTAssertFalse(false ||| false)
35 | }
36 |
37 | func testInclusingOrForBooleansOperator_ReturnsTrue_WhenFirstArgumentIsTrueAndSecondIsTrue() {
38 | XCTAssertTrue(true ||| true)
39 | }
40 |
41 | func testInclusingOrForBooleansOperator_ReturnsTrue_WhenFirstArgumentIsFalseAndSecondIsTrue() {
42 | XCTAssertTrue(false ||| true)
43 | }
44 |
45 | func testInclusingOrForBooleansOperator_ReturnsTrue_WhenFirstArgumentIsTrueAndSecondIsFalse() {
46 | XCTAssertTrue(true ||| false)
47 | }
48 |
49 | func testInclusingOrForBooleansOperator_ReturnsTrue_WhenFirstArgumentIsNilAndSecondIsTrue() {
50 | XCTAssertTrue(nil ||| true)
51 | }
52 |
53 | func testInclusingOrForBooleansOperator_ReturnsTrue_WhenFirstArgumentIsTrueAndSecondIsNil() {
54 | XCTAssertTrue(true ||| nil)
55 | }
56 |
57 | func testInclusingOrForBooleansOperator_ReturnsFalse_WhenFirstArgumentIsNilAndSecondIsFalse() {
58 | XCTAssertFalse(nil ||| false)
59 | }
60 |
61 | func testInclusingOrForBooleansOperator_ReturnsFalse_WhenFirstArgumentIsFalseAndSecondIsNil() {
62 | XCTAssertFalse(false ||| nil)
63 | }
64 |
65 | func testInclusingOrForBooleansOperator_ReturnsFalse_WhenBothArgumentsAreNil() {
66 | XCTAssertFalse(nil ||| nil)
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/Example/JustTweak/UI/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Example/JustTweak/TweakProviders/LocalTweaks_example.json:
--------------------------------------------------------------------------------
1 | {
2 | "ui_customization": {
3 | "display_red_view": {
4 | "Title": "Display Red View",
5 | "Description": "shows a red view in the main view controller",
6 | "Group": "UI Customization",
7 | "Value": false,
8 | "GeneratedPropertyName": "canShowRedView"
9 | },
10 | "display_yellow_view": {
11 | "Title": "Display Yellow View",
12 | "Description": "shows a yellow view in the main view controller",
13 | "Group": "UI Customization",
14 | "Value": false,
15 | "GeneratedPropertyName": "canShowYellowView"
16 | },
17 | "display_green_view": {
18 | "Title": "Display Green View",
19 | "Description": "shows a green view in the main view controller",
20 | "Group": "UI Customization",
21 | "Value": true,
22 | "GeneratedPropertyName": "canShowGreenView"
23 | },
24 | "red_view_alpha_component": {
25 | "Title": "Red View Alpha Component",
26 | "Description": "defines the alpha level of the red view",
27 | "Group": "UI Customization",
28 | "Value": 1.0,
29 | "GeneratedPropertyName": "redViewAlpha"
30 | },
31 | "label_text": {
32 | "Title": "Label Text",
33 | "Description": "the title of the main label",
34 | "Group": "UI Customization",
35 | "Value": "Test value"
36 | }
37 | },
38 | "general": {
39 | "encrypted_answer_to_the_universe": {
40 | "Title": "Encrypted definitive answer",
41 | "Description": "Encrypted answer to the Ultimate Question of Life, the Universe, and Everything",
42 | "Group": "General",
43 | "Value": "24 ton yletinifeD",
44 | "GeneratedPropertyName": "definitiveAnswerEncrypted",
45 | "Encrypted": true
46 | },
47 | "answer_to_the_universe": {
48 | "Title": "Definitive answer",
49 | "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything",
50 | "Group": "General",
51 | "Value": 42,
52 | "GeneratedPropertyName": "meaningOfLife"
53 | },
54 | "greet_on_app_did_become_active": {
55 | "Title": "Greet on app launch",
56 | "Description": "shows an alert on applicationDidBecomeActive",
57 | "Group": "General",
58 | "Value": false,
59 | "GeneratedPropertyName": "shouldShowAlert"
60 | },
61 | "tap_to_change_color_enabled": {
62 | "Title": "Tap to change views color",
63 | "Description": "change the colour of the main view when receiving a tap",
64 | "Group": "General",
65 | "Value": true,
66 | "GeneratedPropertyName": "isTapGestureToChangeColorEnabled"
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Framework/Sources/Utilities/PropertyWrappers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PropertyWrappers.swift
3 | // Copyright (c) 2019 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | @propertyWrapper
9 | public struct TweakProperty {
10 | let feature: String
11 | let variable: String
12 | let tweakManager: TweakManager
13 |
14 | public init(feature: String, variable: String, tweakManager: TweakManager) {
15 | self.feature = feature
16 | self.variable = variable
17 | self.tweakManager = tweakManager
18 | }
19 |
20 | public var wrappedValue: T {
21 | get {
22 | let tweak = try? tweakManager.tweakWith(feature: feature, variable: variable)
23 | return tweak?.value as! T
24 | }
25 | set {
26 | tweakManager.set(newValue, feature: feature, variable: variable)
27 | }
28 | }
29 | }
30 |
31 | @propertyWrapper
32 | public struct FallbackTweakProperty {
33 | let fallbackValue: T
34 | let feature: String
35 | let variable: String
36 | let tweakManager: TweakManager
37 |
38 | public init(fallbackValue: T, feature: String, variable: String, tweakManager: TweakManager) {
39 | self.fallbackValue = fallbackValue
40 | self.feature = feature
41 | self.variable = variable
42 | self.tweakManager = tweakManager
43 | }
44 |
45 | public var wrappedValue: T {
46 | get {
47 | let tweak = try? tweakManager.tweakWith(feature: feature, variable: variable)
48 | return (tweak?.value as? T) ?? fallbackValue
49 | }
50 | set {
51 | tweakManager.set(newValue, feature: feature, variable: variable)
52 | }
53 | }
54 | }
55 |
56 | @propertyWrapper
57 | public struct OptionalTweakProperty {
58 | let fallbackValue: T?
59 | let feature: String
60 | let variable: String
61 | let tweakManager: TweakManager
62 |
63 | public init(fallbackValue: T?, feature: String, variable: String, tweakManager: TweakManager) {
64 | self.fallbackValue = fallbackValue
65 | self.feature = feature
66 | self.variable = variable
67 | self.tweakManager = tweakManager
68 | }
69 |
70 | public var wrappedValue: T? {
71 | get {
72 | let tweak = try? tweakManager.tweakWith(feature: feature, variable: variable)
73 | return (tweak?.value as? T) ?? fallbackValue
74 | }
75 | set {
76 | if let newValue = newValue {
77 | tweakManager.set(newValue, feature: feature, variable: variable)
78 | }
79 | else {
80 | tweakManager.deleteValue(feature: feature, variable: variable)
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Example/Tests/Core/TweakExtensionsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakExtensionsTests.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | import JustTweak
8 |
9 | class TweakExtensionsTests: XCTestCase {
10 |
11 | func testString_RepresentingBool_CanBeConvertedToTweakValue_True() {
12 | XCTAssertTrue("true".tweakValue.boolValue)
13 | }
14 |
15 | func testString_RepresentingBool_CanBeConvertedToTweakValue_False() {
16 | XCTAssertFalse("false".tweakValue.boolValue)
17 | }
18 |
19 | func testString_RepresentingBool_CanBeConvertedToTweakValue_MixedCaseTrue() {
20 | XCTAssertTrue("TrUe".tweakValue.boolValue)
21 | }
22 |
23 | func testString_RepresentingBool_CanBeConvertedToTweakValue_MixedCaseFalse() {
24 | XCTAssertFalse("fAlSe".tweakValue.boolValue)
25 | }
26 |
27 | func testString_RepresentingNumber_CanBeConvertedToTweakValue_Double() {
28 | XCTAssertTrue("1.0".tweakValue == 1.0)
29 | }
30 |
31 | func testString_RepresentingNumber_CanBeConvertedToTweakValue_Int() {
32 | XCTAssertTrue("1".tweakValue == 1)
33 | }
34 |
35 | func testString_ReturnsSelfAsTweakValue_IfDoesNotRepresentAnyOtherTypeOfTweakValue() {
36 | XCTAssertTrue("hello".tweakValue == "hello")
37 | }
38 |
39 | func testIntValueCanBeConvertedIntoNumber() {
40 | let value: Int = 1
41 | let tweakValue: TweakValue = value
42 | XCTAssertEqual(NSNumber(value: value), NSNumber(tweakValue: tweakValue))
43 | }
44 |
45 | func testFloatValueCanBeConvertedIntoNumber() {
46 | let value: Float = 1
47 | let tweakValue: TweakValue = value
48 | XCTAssertEqual(NSNumber(value: value), NSNumber(tweakValue: tweakValue))
49 | }
50 |
51 | func testDoubleValueCanBeConvertedIntoNumber() {
52 | let value: Double = 1
53 | let tweakValue: TweakValue = value
54 | XCTAssertEqual(NSNumber(value: value), NSNumber(tweakValue: tweakValue))
55 | }
56 |
57 | func testBoolValueCanBeConvertedIntoNumber() {
58 | let value = true
59 | let tweakValue: TweakValue = value
60 | XCTAssertEqual(NSNumber(value: value), NSNumber(tweakValue: tweakValue))
61 | }
62 |
63 | func testStringValueCannotBeConvertedIntoNumber() {
64 | let tweakValue: TweakValue = "Hello"
65 | XCTAssertNil(NSNumber(tweakValue: tweakValue))
66 | }
67 |
68 | func testNumberCanBeConvertedIntoBoolTweak() {
69 | let tweakValue: TweakValue = true
70 | XCTAssertTrue(NSNumber(value: true).tweakValue == tweakValue)
71 | XCTAssertTrue(NSNumber(value: false).tweakValue == false)
72 | }
73 |
74 | func testNumberCanBeConvertedIntoIntTweak() {
75 | XCTAssertTrue(NSNumber(value: 1).tweakValue == 1)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakLoader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakLoader.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | extension String: Error {}
9 |
10 | class TweakLoader {
11 |
12 | func load(_ filePath: String) throws -> [Tweak] {
13 | let url = URL(fileURLWithPath: filePath)
14 | let data = try Data(contentsOf: url)
15 | guard let content = try JSONSerialization.jsonObject(with: data) as? TweaksFormat else {
16 | throw "Invalid JSON format for file \(filePath)"
17 | }
18 |
19 | let tweaks = try content.map { (featureKey: String, tweaks: [String: [String: Any]]) throws -> [Tweak] in
20 | try tweaks.map { (variableKey: String, value: [String: Any]) throws -> Tweak in
21 | try tweak(for: value, feature: featureKey, variable: variableKey)
22 | }
23 | .sorted { $0.variable < $1.variable }
24 | }
25 | .flatMap { $0 }
26 | .sorted { $0.feature < $1.feature }
27 |
28 | try validate(tweaks)
29 |
30 | return tweaks
31 | }
32 |
33 | func validate(_ tweaks: [Tweak]) throws {
34 | let propertyNames = tweaks.map { $0.propertyName }.compactMap { $0 }
35 | let duplicates = propertyNames.duplicates
36 | if duplicates.count > 0 {
37 | throw "Found duplicate 'GeneratedPropertyName': \(duplicates)"
38 | }
39 | }
40 |
41 | func type(for value: Any) throws -> String {
42 | switch value {
43 | case _ as String: return "String"
44 | case let numberValue as NSNumber: return numberValue.tweakType
45 | case _ as Bool: return "Bool"
46 | case _ as Double: return "Double"
47 | default:
48 | throw "Unsupported value type \(Swift.type(of: value))"
49 | }
50 | }
51 |
52 | func tweak(for dictionary: [String: Any], feature: FeatureKey, variable: VariableKey) throws -> Tweak {
53 | guard let title = dictionary["Title"] as? String else {
54 | throw "Missing 'Title' value in dictionary \(dictionary)"
55 | }
56 | guard let group = dictionary["Group"] as? String else {
57 | throw "Missing 'Group' value in dictionary \(dictionary)"
58 | }
59 | guard let value = dictionary["Value"] else {
60 | throw "Missing 'Value' value in dictionary \(dictionary)"
61 | }
62 | return Tweak(feature: feature,
63 | variable: variable,
64 | title: title,
65 | description: dictionary["Description"] as? String,
66 | group: group,
67 | valueType: try type(for: value),
68 | propertyName: dictionary["GeneratedPropertyName"] as? String)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/GeneratedTweakAccessorContent:
--------------------------------------------------------------------------------
1 | // GeneratedTweakAccessorContent.swift
2 |
3 | /// Generated by TweakAccessorGenerator
4 |
5 | import Foundation
6 | import JustTweak
7 |
8 | class GeneratedTweakAccessorContent {
9 |
10 | private(set) var tweakManager: TweakManager
11 |
12 | init(with tweakManager: TweakManager) {
13 | self.tweakManager = tweakManager
14 | }
15 |
16 | var canShowGreenView: Bool {
17 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayGreenView))?.boolValue ?? false }
18 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayGreenView) }
19 | }
20 |
21 | var canShowRedView: Bool {
22 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView))?.boolValue ?? false }
23 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayRedView) }
24 | }
25 |
26 | var canShowYellowView: Bool {
27 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView))?.boolValue ?? false }
28 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayYellowView) }
29 | }
30 |
31 | var isTapGestureToChangeColorEnabled: Bool {
32 | get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.tapToChangeColorEnabled))?.boolValue ?? false }
33 | set { tweakManager.set(newValue, feature: Features.general, variable: Variables.tapToChangeColorEnabled) }
34 | }
35 |
36 | var labelText: String {
37 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.labelText))?.stringValue ?? "" }
38 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.labelText) }
39 | }
40 |
41 | var meaningOfLife: Int {
42 | get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.answerToTheUniverse))?.intValue ?? 0 }
43 | set { tweakManager.set(newValue, feature: Features.general, variable: Variables.answerToTheUniverse) }
44 | }
45 |
46 | var redViewAlpha: Double {
47 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.redViewAlphaComponent))?.doubleValue ?? 0.0 }
48 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.redViewAlphaComponent) }
49 | }
50 |
51 | var shouldShowAlert: Bool {
52 | get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.greetOnAppDidBecomeActive))?.boolValue ?? false }
53 | set { tweakManager.set(newValue, feature: Features.general, variable: Variables.greetOnAppDidBecomeActive) }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakAccessorCodeGeneratorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakAccessorCodeGeneratorTests.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import XCTest
7 |
8 | class TweakAccessorCodeGeneratorTests: XCTestCase {
9 |
10 | private var bundle: Bundle!
11 | private let tweaksFilename = "ValidTweaks_LowPriority"
12 | private var tweaksFilePath: String!
13 | private let generatedClassName = "GeneratedTweakAccessor"
14 | private var codeGenerator: TweakAccessorCodeGenerator!
15 | private var tweakLoader: TweakLoader!
16 | private var tweaks: [Tweak]!
17 |
18 | override func setUpWithError() throws {
19 | try super.setUpWithError()
20 | bundle = Bundle(for: type(of: self))
21 | tweaksFilePath = try XCTUnwrap(bundle.path(forResource: tweaksFilename, ofType: "json"))
22 | codeGenerator = TweakAccessorCodeGenerator()
23 | tweakLoader = TweakLoader()
24 | tweaks = try tweakLoader.load(tweaksFilePath)
25 | }
26 |
27 | override func tearDownWithError() throws {
28 | bundle = nil
29 | tweaksFilePath = nil
30 | codeGenerator = nil
31 | tweakLoader = nil
32 | tweaks = nil
33 | try super.tearDownWithError()
34 | }
35 |
36 | func test_generateConstants_output() throws {
37 | let configuration = Configuration(accessorName: "GeneratedTweakAccessorContent")
38 | let content = codeGenerator.generateConstantsFileContent(tweaks: tweaks, configuration: configuration)
39 | let testContentPath = try XCTUnwrap(bundle.path(forResource: "GeneratedTweakAccessor+ConstantsContent", ofType: ""))
40 | let testContent = try String(contentsOfFile: testContentPath, encoding: .utf8)
41 | XCTAssertEqual(content, testContent)
42 | }
43 |
44 | func test_generateAccessor_output() throws {
45 | let configuration = Configuration(accessorName: "GeneratedTweakAccessorContent")
46 | let content = codeGenerator.generateAccessorFileContent(tweaksFilename: tweaksFilename,
47 | tweaks: tweaks,
48 | configuration: configuration)
49 | let testContentPath = try XCTUnwrap(bundle.path(forResource: "GeneratedTweakAccessorContent", ofType: ""))
50 | let testContent = try String(contentsOfFile: testContentPath, encoding: .utf8)
51 |
52 | XCTAssertEqual(content, testContent)
53 | }
54 |
55 | private func codeBlock(for customTweakProviderFile: String) throws -> String {
56 | let testBundle = Bundle(for: TweakAccessorCodeGeneratorTests.self)
57 | let filePath = try XCTUnwrap(testBundle.path(forResource: customTweakProviderFile, ofType: ""))
58 | return try String(contentsOfFile: filePath)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Example/JustTweak/TweakProviders/FirebaseTweakProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FirebaseTweakProvider.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import JustTweak
7 | import FirebaseCore
8 | import FirebaseRemoteConfig
9 |
10 | public class FirebaseTweakProvider: TweakProvider {
11 |
12 | public init() {
13 | /* DOWNLOAD THE GoogleService.plist from the Firebase dashboard */
14 | let googleServicePlistURL = Bundle.main.url(forResource: "GoogleService-Info", withExtension: "plist")
15 | if let _ = googleServicePlistURL {
16 | firebaseAppClass.configure()
17 | configured = true
18 | fetchTweaks()
19 | }
20 | else {
21 | logClosure?("\(self) couldn't find a GoogleService Plist. This is required for this configuration to function. No Tweak will be returned from queries.", .error)
22 | }
23 | }
24 |
25 | public var logClosure: LogClosure?
26 |
27 | // Google dependencies
28 | private var configured: Bool = false
29 | internal lazy var firebaseAppClass: FirebaseApp.Type = {
30 | return FirebaseApp.self
31 | }()
32 | internal lazy var remoteConfiguration: RemoteConfig = {
33 | return RemoteConfig.remoteConfig()
34 | }()
35 |
36 | private func fetchTweaks() {
37 | guard configured else { return }
38 | remoteConfiguration.configSettings = RemoteConfigSettings()
39 | remoteConfiguration.fetch { [weak self] (status, error) in
40 | guard let self = self else { return }
41 | if let error = error {
42 | self.logClosure?("Error while fetching Firebase configuration => \(error)", .error)
43 | }
44 | else {
45 | self.remoteConfiguration.activate(completion: nil) // You can pass a completion handler if you want the configuration to be applied immediately after being fetched; otherwise it will be applied on next launch
46 | let notificationCentre = NotificationCenter.default
47 | notificationCentre.post(name: TweakProviderDidChangeNotification, object: self)
48 | }
49 | }
50 | }
51 |
52 | public func isFeatureEnabled(_ feature: String) -> Bool {
53 | let configValue = remoteConfiguration.configValue(forKey: feature)
54 | guard configValue.source != .static else { return false }
55 | return configValue.boolValue
56 | }
57 |
58 | public func tweakWith(feature: String, variable: String) throws -> Tweak {
59 | guard configured else { throw TweakError.notFound }
60 | let configValue = remoteConfiguration.configValue(forKey: variable)
61 | guard configValue.source != .static else { return nil }
62 | guard let stringValue = configValue.stringValue else { return nil }
63 | return Tweak(feature: feature,
64 | variable: variable,
65 | value: stringValue.tweakValue,
66 | title: nil,
67 | group: nil)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Framework/Sources/UI/Cells/TextTweakTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextTweakTableViewCell
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import UIKit
7 |
8 | class TextTweakTableViewCell: UITableViewCell, TweakViewControllerCell, UITextFieldDelegate {
9 |
10 | var feature: String?
11 | var variable: String?
12 |
13 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
14 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
15 | }
16 |
17 | required init?(coder aDecoder: NSCoder) {
18 | fatalError("init(coder:) has not been implemented")
19 | }
20 |
21 | var title: String? {
22 | get {
23 | return textLabel?.text
24 | }
25 | set {
26 | textLabel?.text = newValue
27 | }
28 | }
29 |
30 | var desc: String? {
31 | get {
32 | return detailTextLabel?.text
33 | }
34 | set {
35 | detailTextLabel?.text = newValue
36 | }
37 | }
38 |
39 | var value: TweakValue = "" {
40 | didSet {
41 | textField.text = value.description
42 | textField.sizeToFit()
43 | if textField.bounds.width > bounds.width / 2.0 {
44 | textField.bounds.size = CGSize(width: bounds.width / 2.0,
45 | height: textField.bounds.size.height)
46 | }
47 | }
48 | }
49 | weak var delegate: TweakViewControllerCellDelegate?
50 |
51 | var keyboardType: UIKeyboardType {
52 | get {
53 | return .default
54 | }
55 | }
56 |
57 | lazy var textField: UITextField! = {
58 | let textField = UITextField()
59 | textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
60 | textField.addTarget(self, action: #selector(textEditingDidEnd), for: .editingDidEnd)
61 | textField.textColor = UIColor.darkGray
62 | textField.keyboardType = self.keyboardType
63 | textField.returnKeyType = .done
64 | textField.textAlignment = .right
65 | textField.borderStyle = .none
66 | textField.delegate = self
67 | self.accessoryView = textField
68 | self.selectionStyle = .none
69 | return textField
70 | }()
71 |
72 | @objc func textDidChange() {
73 | guard let text = textField.text else { return }
74 | if let int = Int(text) {
75 | value = int
76 | }
77 | else if let double = Double(text) {
78 | value = double
79 | }
80 | else if let float = Float(text) {
81 | value = float
82 | }
83 | else {
84 | value = text
85 | }
86 | }
87 |
88 | @objc func textEditingDidEnd() {
89 | delegate?.tweakConfigurationCellDidChangeValue(self)
90 | }
91 |
92 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
93 | textField.endEditing(true)
94 | return true
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/Example/JustTweak/Code/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import JustTweak
7 | import UIKit
8 |
9 | @UIApplicationMain
10 | class AppDelegate: UIResponder, UIApplicationDelegate {
11 |
12 | var window: UIWindow?
13 | var tweakAccessor: GeneratedTweakAccessor!
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
16 | let navigationController = window?.rootViewController as! UINavigationController
17 | let viewController = navigationController.topViewController as! ViewController
18 | tweakAccessor = GeneratedTweakAccessor(with: makeTweakManager())
19 | viewController.tweakAccessor = tweakAccessor
20 | viewController.tweakManager = tweakAccessor.tweakManager
21 | return true
22 | }
23 |
24 | func applicationDidBecomeActive(_ application: UIApplication) {
25 | if tweakAccessor.shouldShowAlert {
26 | let alertController = UIAlertController(title: "Hello",
27 | message: "Welcome to this Demo app!",
28 | preferredStyle: .alert)
29 | alertController.addAction(UIAlertAction(title: "Continue", style: .default, handler: nil))
30 | window?.rootViewController?.present(alertController, animated: true, completion: nil)
31 | }
32 | }
33 |
34 | func makeTweakManager() -> TweakManager {
35 | var tweakProviders: [TweakProvider] = []
36 |
37 | // EphemeralTweakProvider
38 | #if DEBUG || CONFIGURATION_UI_TESTS
39 | let ephemeralTweakProvider_1 = NSMutableDictionary()
40 | tweakProviders.append(ephemeralTweakProvider_1)
41 | #endif
42 |
43 | // UserDefaultsTweakProvider
44 | #if DEBUG || CONFIGURATION_DEBUG
45 | let userDefaultsTweakProvider_1 = UserDefaultsTweakProvider(userDefaults: UserDefaults.standard)
46 | tweakProviders.append(userDefaultsTweakProvider_1)
47 | #endif
48 |
49 | // LocalTweakProvider
50 | #if DEBUG
51 | let jsonFileURL_1 = Bundle.main.url(forResource: "LocalTweaks_TopPriority_example", withExtension: "json")!
52 | let localTweakProvider_1 = LocalTweakProvider(jsonURL: jsonFileURL_1)
53 | tweakProviders.append(localTweakProvider_1)
54 | #endif
55 |
56 | // LocalTweakProvider
57 | let jsonFileURL_2 = Bundle.main.url(forResource: "LocalTweaks_example", withExtension: "json")!
58 | let localTweakProvider_2 = LocalTweakProvider(jsonURL: jsonFileURL_2)
59 | tweakProviders.append(localTweakProvider_2)
60 |
61 | let tweakManager = TweakManager(tweakProviders: tweakProviders)
62 | tweakManager.useCache = true
63 |
64 | tweakManager.decryptionClosure = { tweak in
65 | String((tweak.value.stringValue ?? "").reversed())
66 | }
67 |
68 | return tweakManager
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Framework/Sources/TweakProviders/UserDefaultsTweakProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsTweakProvider.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | final public class UserDefaultsTweakProvider {
9 |
10 | private let userDefaults: UserDefaults
11 |
12 | private static let userDefaultsKeyPrefix = "lib.fragments.userDefaultsKey"
13 |
14 | public var logClosure: LogClosure?
15 | public var decryptionClosure: ((Tweak) -> TweakValue)?
16 |
17 | public init(userDefaults: UserDefaults) {
18 | self.userDefaults = userDefaults
19 | }
20 | }
21 |
22 | extension UserDefaultsTweakProvider: TweakProvider {
23 |
24 | public func isFeatureEnabled(_ feature: String) -> Bool {
25 | let userDefaultsKey = keyForTweakWithIdentifier(feature)
26 | return userDefaults.bool(forKey: userDefaultsKey)
27 | }
28 |
29 | public func tweakWith(feature: String, variable: String) throws -> Tweak {
30 | let userDefaultsKey = keyForTweakWithIdentifier(variable)
31 | let userDefaultsValue = userDefaults.object(forKey: userDefaultsKey) as AnyObject?
32 | guard let value = updateUserDefaults(userDefaultsValue) else { throw TweakError.notFound }
33 |
34 | return Tweak(
35 | feature: feature,
36 | variable: variable,
37 | value: value,
38 | title: nil,
39 | group: nil
40 | )
41 | }
42 | }
43 |
44 | extension UserDefaultsTweakProvider: MutableTweakProvider {
45 |
46 | public func set(_ value: TweakValue, feature: String, variable: String) {
47 | updateUserDefaults(value: value, feature: feature, variable: variable)
48 | }
49 |
50 | public func deleteValue(feature: String, variable: String) {
51 | userDefaults.removeObject(forKey: keyForTweakWithIdentifier(variable))
52 | }
53 | }
54 |
55 | extension UserDefaultsTweakProvider {
56 |
57 | private func keyForTweakWithIdentifier(_ identifier: String) -> String {
58 | return "\(UserDefaultsTweakProvider.userDefaultsKeyPrefix).\(identifier)"
59 | }
60 |
61 | private func updateUserDefaults(_ object: AnyObject?) -> TweakValue? {
62 | if let object = object as? String {
63 | return object
64 | }
65 | else if let object = object as? NSNumber {
66 | return object.tweakValue
67 | }
68 | return nil
69 | }
70 |
71 | private func updateUserDefaults(value: TweakValue, feature: String, variable: String) {
72 | userDefaults.set(value, forKey: keyForTweakWithIdentifier(variable))
73 | DispatchQueue.main.async {
74 | let notificationCenter = NotificationCenter.default
75 | let tweak = Tweak(feature: feature, variable: variable, value: value)
76 | let userInfo = [TweakProviderDidChangeNotificationTweakKey: tweak]
77 | notificationCenter.post(name: TweakProviderDidChangeNotification,
78 | object: self,
79 | userInfo: userInfo)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift:
--------------------------------------------------------------------------------
1 | // GeneratedTweakAccessor.swift
2 |
3 | /// Generated by TweakAccessorGenerator
4 |
5 | import Foundation
6 | import JustTweak
7 |
8 | class GeneratedTweakAccessor {
9 |
10 | private(set) var tweakManager: TweakManager
11 |
12 | init(with tweakManager: TweakManager) {
13 | self.tweakManager = tweakManager
14 | }
15 |
16 | var canShowGreenView: Bool {
17 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayGreenView))?.boolValue ?? false }
18 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayGreenView) }
19 | }
20 |
21 | var canShowRedView: Bool {
22 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView))?.boolValue ?? false }
23 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayRedView) }
24 | }
25 |
26 | var canShowYellowView: Bool {
27 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView))?.boolValue ?? false }
28 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayYellowView) }
29 | }
30 |
31 | var definitiveAnswerEncrypted: String {
32 | get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.encryptedAnswerToTheUniverse))?.stringValue ?? "" }
33 | set { tweakManager.set(newValue, feature: Features.general, variable: Variables.encryptedAnswerToTheUniverse) }
34 | }
35 |
36 | var isTapGestureToChangeColorEnabled: Bool {
37 | get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.tapToChangeColorEnabled))?.boolValue ?? false }
38 | set { tweakManager.set(newValue, feature: Features.general, variable: Variables.tapToChangeColorEnabled) }
39 | }
40 |
41 | var labelText: String {
42 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.labelText))?.stringValue ?? "" }
43 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.labelText) }
44 | }
45 |
46 | var meaningOfLife: Int {
47 | get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.answerToTheUniverse))?.intValue ?? 0 }
48 | set { tweakManager.set(newValue, feature: Features.general, variable: Variables.answerToTheUniverse) }
49 | }
50 |
51 | var redViewAlpha: Double {
52 | get { (try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.redViewAlphaComponent))?.doubleValue ?? 0.0 }
53 | set { tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.redViewAlphaComponent) }
54 | }
55 |
56 | var shouldShowAlert: Bool {
57 | get { (try? tweakManager.tweakWith(feature: Features.general, variable: Variables.greetOnAppDidBecomeActive))?.boolValue ?? false }
58 | set { tweakManager.set(newValue, feature: Features.general, variable: Variables.greetOnAppDidBecomeActive) }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Example/Tests/Core/LocalTweakProviderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocalTweakProviderTests.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | import JustTweak
8 |
9 | class LocalTweakProviderTests: XCTestCase {
10 |
11 | private var tweakProvider: LocalTweakProvider!
12 |
13 | override func setUp() {
14 | super.setUp()
15 | tweakProvider = tweakProviderWithFileNamed("LocalTweaks_test")!
16 | }
17 |
18 | override func tearDown() {
19 | tweakProvider = nil
20 | super.tearDown()
21 | }
22 |
23 | private func tweakProviderWithFileNamed(_ fileName: String) -> LocalTweakProvider? {
24 | let bundle = Bundle(for: LocalTweakProviderTests.self)
25 | let jsonURL = bundle.url(forResource: fileName, withExtension: "json")!
26 | return LocalTweakProvider(jsonURL: jsonURL)
27 | }
28 |
29 | func testParsesBoolTweak() {
30 | let redViewTweak = Tweak(feature: Features.uiCustomization,
31 | variable: Variables.displayRedView,
32 | value: true,
33 | title: "Display Red View",
34 | group: "UI Customization")
35 | XCTAssertEqual(redViewTweak, try tweakProvider.tweakWith(feature: Features.uiCustomization,
36 | variable: Variables.displayRedView))
37 | }
38 |
39 | func testParsesFloatTweak() {
40 | let redViewAlphaTweak = Tweak(feature: Features.uiCustomization,
41 | variable: Variables.redViewAlpha,
42 | value: 1.0,
43 | title: "Red View Alpha Component",
44 | group: "UI Customization")
45 | XCTAssertEqual(redViewAlphaTweak, try tweakProvider.tweakWith(feature: Features.uiCustomization,
46 | variable: Variables.redViewAlpha))
47 | }
48 |
49 | func testParsesStringTweak() {
50 | let buttonLabelTweak = Tweak(feature: Features.uiCustomization,
51 | variable: Variables.labelText,
52 | value: "Test value",
53 | title: "Label Text", group: "UI Customization")
54 | XCTAssertEqual(buttonLabelTweak, try tweakProvider.tweakWith(feature: Features.uiCustomization,
55 | variable: Variables.labelText))
56 | }
57 |
58 | func testDecryptionClosure() {
59 | XCTAssertNil(tweakProvider.decryptionClosure)
60 |
61 | tweakProvider.decryptionClosure = { tweak in
62 | (tweak.value.stringValue ?? "") + "Decrypted"
63 | }
64 |
65 | let tweak = Tweak(feature: "feature", variable: "variable", value: "topSecret")
66 | let decryptedTweak = tweakProvider.decryptionClosure?(tweak)
67 |
68 | XCTAssertEqual(decryptedTweak?.stringValue, "topSecretDecrypted")
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Framework/Sources/DTOs/Tweak.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tweak.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | public struct Tweak {
9 |
10 | public let feature: String
11 | public let variable: String
12 |
13 | public let value: TweakValue
14 |
15 | public let title: String?
16 | public let desc: String?
17 | public let group: String?
18 | public let source: String?
19 |
20 | public var displayTitle: String {
21 | return title ?? "\(feature):\(variable)"
22 | }
23 |
24 | private var dictionaryValue: [String : Any?] {
25 | get {
26 | return ["feature": feature,
27 | "variable": variable,
28 | "value": value,
29 | "title": title,
30 | "description": desc,
31 | "group": group,
32 | "source": source
33 | ]
34 | }
35 | }
36 |
37 | public init(feature: String,
38 | variable: String,
39 | value: TweakValue,
40 | title: String? = nil,
41 | description: String? = nil,
42 | group: String? = nil,
43 | source: String? = nil) {
44 | self.feature = feature
45 | self.variable = variable
46 | self.value = value
47 | self.title = title
48 | self.desc = description
49 | self.group = group
50 | self.source = source
51 | }
52 |
53 | func mutatedCopy(feature: String? = nil,
54 | variable: String? = nil,
55 | value: TweakValue? = nil,
56 | title: String? = nil,
57 | description: String? = nil,
58 | group: String? = nil,
59 | source: String? = nil) -> Self {
60 | Self(feature: feature ?? self.feature,
61 | variable: variable ?? self.variable,
62 | value: value ?? self.value,
63 | title: title ?? self.title,
64 | description: description ?? self.desc,
65 | group: group ?? self.group,
66 | source: source ?? self.source)
67 | }
68 | }
69 |
70 | extension Tweak: CustomStringConvertible {
71 |
72 | public var description: String {
73 | get {
74 | return dictionaryValue.description
75 | }
76 | }
77 | }
78 |
79 | extension Tweak: Equatable {
80 |
81 | public static func ==(lhs: Self, rhs: Self) -> Bool {
82 | return lhs.feature == rhs.feature &&
83 | lhs.variable == rhs.variable &&
84 | lhs.value == rhs.value &&
85 | lhs.title == rhs.title &&
86 | lhs.desc == rhs.desc &&
87 | lhs.group == rhs.group &&
88 | lhs.source == rhs.source
89 | }
90 | }
91 |
92 | public extension Tweak {
93 |
94 | var intValue: Int {
95 | return value.intValue
96 | }
97 |
98 | var floatValue: Float {
99 | return value.floatValue
100 | }
101 |
102 | var doubleValue: Double {
103 | return value.doubleValue
104 | }
105 |
106 | var boolValue: Bool {
107 | return value.boolValue
108 | }
109 |
110 | var stringValue: String? {
111 | return value.stringValue
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Framework/Sources/TweakProviders/LocalTweakProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocalTweakProvider.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | final public class LocalTweakProvider {
9 |
10 | private enum EncodingKeys : String {
11 | case Title, Description, Group, Value, Encrypted
12 | }
13 |
14 | private let configurationFile: [String : [String : [String : AnyObject]]]
15 | private let fileURL: URL
16 |
17 | public var logClosure: LogClosure?
18 | public var decryptionClosure: ((Tweak) -> TweakValue)?
19 |
20 | public var features: [String : [String]] {
21 | var storage: [String : [String]] = [:]
22 | for feature in Array(configurationFile.keys) {
23 | for variable in Array(configurationFile[feature]!.keys) {
24 | if let _ = storage[feature] {
25 | storage[feature]?.append(variable)
26 | } else {
27 | storage[feature] = [variable]
28 | }
29 | }
30 | }
31 | return storage
32 | }
33 |
34 | public init(jsonURL: URL) {
35 | let data = try! Data(contentsOf: jsonURL)
36 | let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
37 | let configuration = json as! [String : [String : [String : AnyObject]]]
38 | configurationFile = configuration
39 | fileURL = jsonURL
40 | }
41 |
42 | private func tweakValueFromJSONObject(_ jsonObject: AnyObject?) -> TweakValue {
43 | let value: TweakValue
44 | if let numberValue = jsonObject as? NSNumber {
45 | value = numberValue.tweakValue
46 | }
47 | else if let stringValue = jsonObject as? String {
48 | value = stringValue
49 | }
50 | else {
51 | value = false
52 | }
53 | return value
54 | }
55 | }
56 |
57 | extension LocalTweakProvider: TweakProvider {
58 |
59 | public func isFeatureEnabled(_ feature: String) -> Bool {
60 | return configurationFile[feature] != nil
61 | }
62 |
63 | public func tweakWith(feature: String, variable: String) throws -> Tweak {
64 | guard let entry = configurationFile[feature]?[variable] else { throw TweakError.notFound }
65 | let title = entry[EncodingKeys.Title.rawValue] as? String
66 | let description = entry[EncodingKeys.Description.rawValue] as? String
67 | let group = entry[EncodingKeys.Group.rawValue] as? String
68 | let value = tweakValueFromJSONObject(entry[EncodingKeys.Value.rawValue])
69 | let isEncrypted = (entry[EncodingKeys.Encrypted.rawValue] as? Bool) ?? false
70 |
71 | let tweak = Tweak(feature: feature,
72 | variable: variable,
73 | value: value,
74 | title: title,
75 | description: description,
76 | group: group)
77 |
78 | if isEncrypted {
79 | guard let decryptionClosure = decryptionClosure else {
80 | // The configuration is not correct, it's encrypted, but there's no way to decrypt
81 | throw TweakError.decryptionClosureNotProvided
82 | }
83 |
84 | return tweak.mutatedCopy(value: decryptionClosure(tweak))
85 | } else {
86 | return tweak
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Example/JustTweak/TweakProviders/OptimizelyTweakProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OptimizelyTweakProvider.swift
3 | // Copyright (c) 2018 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import JustTweak
7 | import OptimizelySDKiOS
8 |
9 | public class OptimizelyTweakProvider: TweakProvider {
10 |
11 | private var optimizelyManager: OPTLYManager?
12 | private var optimizelyClient: OPTLYClient?
13 |
14 | public var logClosure: LogClosure?
15 |
16 | public var userId: String!
17 | public var attributes: [String : String]?
18 |
19 | public init() {
20 | /* DOWNLOAD THE Optimizely datafile from the Optimizely dashboard */
21 | optimizelyManager = OPTLYManager(builder: OPTLYManagerBuilder(block: { builder in
22 | guard let builder = builder,
23 | let filePath = Bundle.main.path(forResource: "ExampleOptimizelyDatafile", ofType: "json"),
24 | let fileContents = try? String.init(contentsOfFile: filePath, encoding: .utf8),
25 | let jsonDatafile = fileContents.data(using: .utf8) else { return }
26 | builder.datafile = jsonDatafile
27 | builder.sdkKey = "SDK_KEY_HERE"
28 | }))
29 | optimizelyManager?.initialize() { [weak self] error, client in
30 | guard let strongSelf = self else { return }
31 | switch (error, client) {
32 | case (nil, let client):
33 | strongSelf.optimizelyClient = client
34 | let notificationCentre = NotificationCenter.default
35 | notificationCentre.post(name: TweakProviderDidChangeNotification, object: strongSelf)
36 | case (let error, _):
37 | if let error = error {
38 | strongSelf.logClosure?("Couldn't initialize Optimizely manager. \(error.localizedDescription)", .error)
39 | } else {
40 | strongSelf.logClosure?("Couldn't initialize Optimizely manager", .error)
41 | }
42 | }
43 | }
44 | }
45 |
46 | public func isFeatureEnabled(_ feature: String) -> Bool {
47 | return optimizelyClient?.isFeatureEnabled(feature, userId: userId, attributes: attributes) ?? false
48 | }
49 |
50 | public func tweakWith(feature: String, variable: String) throws -> Tweak {
51 | guard let optimizelyClient = optimizelyClient,
52 | optimizelyClient.isFeatureEnabled(feature, userId: userId, attributes: attributes) == true
53 | else {
54 | throw TweakError.notFound
55 | }
56 |
57 | let tweakValue: TweakValue? = {
58 | if let boolValue = optimizelyClient.getFeatureVariableBoolean(feature, variableKey: variable, userId: userId, attributes: attributes)?.boolValue {
59 | return boolValue
60 | }
61 | else if let doubleValue = optimizelyClient.getFeatureVariableDouble(feature, variableKey: variable, userId: userId, attributes: attributes)?.doubleValue {
62 | return doubleValue
63 | }
64 | else if let intValue = optimizelyClient.getFeatureVariableInteger(feature, variableKey: variable, userId: userId, attributes: attributes)?.intValue {
65 | return intValue
66 | }
67 | else if let stringValue = optimizelyClient.getFeatureVariableString(feature, variableKey: variable, userId: userId, attributes: attributes) {
68 | return stringValue
69 | }
70 | return nil
71 | }()
72 |
73 | guard let tweakValue = tweakValue else {
74 | throw TweakError.notFound
75 | }
76 | return Tweak(feature: feature, variable: variable, value: tweakValue, title: nil, group: nil)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import ArgumentParser
8 |
9 | struct TweakAccessorGenerator: ParsableCommand {
10 |
11 | @Option(name: .shortAndLong, help: "The local tweaks file path.")
12 | var localTweaksFilePath: String
13 |
14 | @Option(name: .shortAndLong, help: "The output folder.")
15 | var outputFolder: String
16 |
17 | @Option(name: .shortAndLong, help: "The configuration folder.")
18 | var configurationFolder: String
19 |
20 | private var tweaksFilename: String {
21 | let url = URL(fileURLWithPath: localTweaksFilePath)
22 | return String(url.lastPathComponent.split(separator: ".").first!)
23 | }
24 |
25 | private var configurationFolderURL: URL {
26 | URL(fileURLWithPath: configurationFolder)
27 | }
28 |
29 | private func loadConfigurationFromJson() -> Configuration {
30 | let configurationUrl = configurationFolderURL.appendingPathComponent("config.json")
31 | let jsonData = try! Data(contentsOf: configurationUrl)
32 | let decodedResult = try! JSONDecoder().decode(Configuration.self, from: jsonData)
33 | return decodedResult
34 | }
35 |
36 | func run() throws {
37 | let codeGenerator = TweakAccessorCodeGenerator()
38 | let tweakLoader = TweakLoader()
39 | let tweaks = try tweakLoader.load(localTweaksFilePath)
40 | let configuration = loadConfigurationFromJson()
41 |
42 | writeConstantsFile(codeGenerator: codeGenerator,
43 | tweaks: tweaks,
44 | outputFolder: outputFolder,
45 | configuration: configuration)
46 |
47 | writeAccessorFile(codeGenerator: codeGenerator,
48 | tweaks: tweaks,
49 | outputFolder: outputFolder,
50 | configuration: configuration)
51 | }
52 | }
53 |
54 | extension TweakAccessorGenerator {
55 |
56 | private func writeConstantsFile(codeGenerator: TweakAccessorCodeGenerator,
57 | tweaks: [Tweak],
58 | outputFolder: String,
59 | configuration: Configuration) {
60 | let fileName = "\(configuration.accessorName)+Constants.swift"
61 | let url: URL = URL(fileURLWithPath: outputFolder).appendingPathComponent(fileName)
62 | let constants = codeGenerator.generateConstantsFileContent(tweaks: tweaks,
63 | configuration: configuration)
64 | if let existingConstants = try? String(contentsOf: url, encoding: .utf8) {
65 | if existingConstants != constants {
66 | try! constants.write(to: url, atomically: true, encoding: .utf8)
67 | }
68 | } else {
69 | try! constants.write(to: url, atomically: true, encoding: .utf8)
70 | }
71 | }
72 |
73 | private func writeAccessorFile(codeGenerator: TweakAccessorCodeGenerator,
74 | tweaks: [Tweak],
75 | outputFolder: String,
76 | configuration: Configuration) {
77 | let fileName = "\(configuration.accessorName).swift"
78 | let url: URL = URL(fileURLWithPath: outputFolder).appendingPathComponent(fileName)
79 | let accessor = codeGenerator.generateAccessorFileContent(tweaksFilename: tweaksFilename,
80 | tweaks: tweaks,
81 | configuration: configuration)
82 | if let existingAccessor = try? String(contentsOf: url, encoding: .utf8) {
83 | if existingAccessor != accessor {
84 | try! accessor.write(to: url, atomically: true, encoding: .utf8)
85 | }
86 | } else {
87 | try! accessor.write(to: url, atomically: true, encoding: .utf8)
88 | }
89 | }
90 | }
91 |
92 | TweakAccessorGenerator.main()
93 |
--------------------------------------------------------------------------------
/Example/JustTweak/Accessors/TweakAccessor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakAccessor.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import JustTweak
8 |
9 | class TweakAccessor {
10 |
11 | static let tweakManager: TweakManager = {
12 | var tweakProviders: [TweakProvider] = []
13 |
14 | // UserDefaultsTweakProvider
15 | #if DEBUG || CONFIGURATION_DEBUG
16 | let userDefaultsTweakProvider_1 = UserDefaultsTweakProvider(userDefaults: UserDefaults.standard)
17 | tweakProviders.append(userDefaultsTweakProvider_1)
18 | #endif
19 |
20 | // OptimizelyTweakProvider
21 | // let optimizelyTweakProvider = OptimizelyTweakProvider()
22 | // optimizelyTweakProvider.userId = UUID().uuidString
23 | // tweakProviders.append(optimizelyTweakProvider)
24 |
25 | // FirebaseTweakProvider
26 | // let firebaseTweakProvider = FirebaseTweakProvider()
27 | // tweakProviders.append(firebaseTweakProvider)
28 |
29 | // LocalTweakProvider
30 | #if CONFIGURATION_DEBUG
31 | let jsonFileURL_1 = Bundle.main.url(forResource: "LocalTweaks_TopPriority_example", withExtension: "json")!
32 | let localTweakProvider_1 = LocalTweakProvider(jsonURL: jsonFileURL_1)
33 | tweakProviders.append(localTweakProvider_1)
34 | #endif
35 |
36 | // LocalTweakProvider
37 | let jsonFileURL_2 = Bundle.main.url(forResource: "LocalTweaks_example", withExtension: "json")!
38 | let localTweakProvider_2 = LocalTweakProvider(jsonURL: jsonFileURL_2)
39 | tweakProviders.append(localTweakProvider_2)
40 |
41 | return TweakManager(tweakProviders: tweakProviders)
42 | }()
43 |
44 | private var tweakManager: TweakManager {
45 | return Self.tweakManager
46 | }
47 |
48 | // MARK: - Via Property Wrappers
49 |
50 | @FallbackTweakProperty(fallbackValue: false,
51 | feature: Features.general,
52 | variable: Variables.greetOnAppDidBecomeActive,
53 | tweakManager: tweakManager)
54 | var shouldShowAlert: Bool
55 |
56 | @FallbackTweakProperty(fallbackValue: false,
57 | feature: Features.uiCustomization,
58 | variable: Variables.displayRedView,
59 | tweakManager: tweakManager)
60 | var canShowRedView: Bool
61 |
62 | @FallbackTweakProperty(fallbackValue: false,
63 | feature: Features.uiCustomization,
64 | variable: Variables.displayGreenView,
65 | tweakManager: tweakManager)
66 | var canShowGreenView: Bool
67 |
68 | @FallbackTweakProperty(fallbackValue: "",
69 | feature: Features.uiCustomization,
70 | variable: Variables.labelText,
71 | tweakManager: tweakManager)
72 | var labelText: String
73 |
74 | @FallbackTweakProperty(fallbackValue: 42,
75 | feature: Features.uiCustomization,
76 | variable: Variables.meaningOfLife,
77 | tweakManager: tweakManager)
78 | var meaningOfLife: Int
79 |
80 | @OptionalTweakProperty(fallbackValue: nil,
81 | feature: Features.uiCustomization,
82 | variable: Variables.answerToTheUniverse,
83 | tweakManager: tweakManager)
84 | var optionalMeaningOfLife: Int?
85 |
86 |
87 | // MARK: - Via TweakManager
88 |
89 | var canShowYellowView: Bool {
90 | return (try? tweakManager.tweakWith(feature: Features.uiCustomization,
91 | variable: Variables.displayYellowView))?.boolValue ?? false
92 | }
93 |
94 | var redViewAlpha: Float {
95 | return (try? tweakManager.tweakWith(feature: Features.uiCustomization,
96 | variable: Variables.redViewAlpha))?.floatValue ?? 0.0
97 | }
98 |
99 | var isTapGestureToChangeColorEnabled: Bool {
100 | return (try? tweakManager.tweakWith(feature: Features.general,
101 | variable: Variables.tapToChangeViewColor))?.boolValue ?? false
102 | }
103 | }
104 |
105 |
--------------------------------------------------------------------------------
/Example/JustTweak/Code/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import UIKit
7 | import JustTweak
8 |
9 | class ViewController: UIViewController {
10 |
11 | @IBOutlet var redView: UIView!
12 | @IBOutlet var greenView: UIView!
13 | @IBOutlet var yellowView: UIView!
14 | @IBOutlet var mainLabel: UILabel!
15 |
16 | var tweakAccessor: GeneratedTweakAccessor!
17 | var tweakManager: TweakManager!
18 |
19 | private var tapGestureRecognizer: UITapGestureRecognizer!
20 |
21 | deinit {
22 | NotificationCenter.default.removeObserver(self)
23 | }
24 |
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 | updateView()
28 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(setAndShowMeaningOfLife))
29 | tapGestureRecognizer.numberOfTapsRequired = 2
30 | view.addGestureRecognizer(tapGestureRecognizer)
31 | tweakManager.registerForConfigurationsUpdates(self) { [weak self] tweak in
32 | print("Tweak changed: \(tweak)")
33 | self?.updateView()
34 | }
35 | addEncryptedMeaningOfLifeTapGesture()
36 | }
37 |
38 | private func addEncryptedMeaningOfLifeTapGesture() {
39 | let tapGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(showEncryptedMeaningOfLife))
40 | view.addGestureRecognizer(tapGestureRecognizer)
41 | }
42 |
43 | internal func updateView() {
44 | setUpGestures()
45 | redView.isHidden = !tweakAccessor.canShowRedView
46 | greenView.isHidden = !tweakAccessor.canShowGreenView
47 | yellowView.isHidden = !tweakAccessor.canShowYellowView
48 | mainLabel.text = tweakAccessor.labelText
49 | redView.alpha = CGFloat(tweakAccessor.redViewAlpha)
50 | }
51 |
52 | internal func setUpGestures() {
53 | if tapGestureRecognizer == nil {
54 | tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(changeViewColor))
55 | view.addGestureRecognizer(tapGestureRecognizer)
56 | }
57 | tapGestureRecognizer.isEnabled = tweakAccessor.isTapGestureToChangeColorEnabled
58 | }
59 |
60 | @objc internal func setAndShowMeaningOfLife() {
61 | tweakAccessor.meaningOfLife = Bool.random() ? 42 : 108
62 | let alertController = UIAlertController(title: "The Meaning of Life",
63 | message: String(describing: tweakAccessor.meaningOfLife),
64 | preferredStyle: .alert)
65 | alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
66 | present(alertController, animated: true, completion: nil)
67 | }
68 |
69 | @objc func showEncryptedMeaningOfLife() {
70 | let alertController = UIAlertController(title: "Encrypted Meaning of Life",
71 | message: String(describing: tweakAccessor.definitiveAnswerEncrypted),
72 | preferredStyle: .alert)
73 | alertController.addAction(UIAlertAction(title: "OK", style: .default))
74 | present(alertController, animated: true)
75 | }
76 |
77 | @objc internal func changeViewColor() {
78 | func randomColorValue() -> CGFloat {
79 | return CGFloat(arc4random() % 255) / 255.0
80 | }
81 | view.backgroundColor = UIColor(red: randomColorValue(),
82 | green: randomColorValue(),
83 | blue: randomColorValue(),
84 | alpha: 1.0)
85 | }
86 |
87 | private var tweakViewController: TweakViewController {
88 | return TweakViewController(style: .grouped, tweakManager: tweakManager)
89 | }
90 |
91 | @IBAction func presentTweakViewController() {
92 | let tweakNavigationController = UINavigationController(rootViewController: tweakViewController)
93 | tweakNavigationController.navigationBar.prefersLargeTitles = true
94 | present(tweakNavigationController, animated: true, completion: nil)
95 | }
96 |
97 | @IBAction func pushTweakViewController() {
98 | navigationController?.pushViewController(tweakViewController, animated: true)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Example/Tests/Core/UserDefaultsTweakProviderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsTweakProviderTests.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | import JustTweak
8 |
9 | class UserDefaultsTweakProviderTests: XCTestCase {
10 |
11 | private var userDefaultsTweakProvider: UserDefaultsTweakProvider!
12 | private let userDefaults = UserDefaults(suiteName: String(describing: UserDefaultsTweakProviderTests.self))!
13 |
14 | private let userDefaultsKeyPrefix = "lib.fragments.userDefaultsKey"
15 |
16 | override func setUp() {
17 | super.setUp()
18 | userDefaultsTweakProvider = UserDefaultsTweakProvider(userDefaults: userDefaults)
19 | }
20 |
21 | override func tearDown() {
22 | userDefaults.removeObject(forKey: "\(userDefaultsKeyPrefix).display_red_view")
23 | userDefaultsTweakProvider = nil
24 | super.tearDown()
25 | }
26 |
27 | func testReturnsCorrectTweaksIdentifiersWhenInitializedAndTweaksHaveBeenSet() throws {
28 | let anotherConfiguration = UserDefaultsTweakProvider(userDefaults: userDefaults)
29 | anotherConfiguration.set("hello", feature: "feature_1", variable: "variable_4")
30 | anotherConfiguration.set(true, feature: "feature_1", variable: "variable_3")
31 | anotherConfiguration.set(12.34, feature: "feature_1", variable: "variable_2")
32 | anotherConfiguration.set(42, feature: "feature_1", variable: "variable_1")
33 |
34 | XCTAssertTrue(try XCTUnwrap(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_1")).value == 42)
35 | XCTAssertTrue(try XCTUnwrap(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_2")).value == 12.34)
36 | XCTAssertTrue(try XCTUnwrap(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_3")).value == true)
37 | XCTAssertTrue(try XCTUnwrap(anotherConfiguration.tweakWith(feature: "feature_1", variable: "variable_4")).value == "hello")
38 | }
39 |
40 | func testReturnsNilForTweaksThatHaveNoUserDefaultValue() {
41 | let tweak = try? userDefaultsTweakProvider.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView)
42 | XCTAssertNil(tweak)
43 | }
44 |
45 | func testUpdatesValueForTweak_withBool() throws {
46 | userDefaultsTweakProvider.set(true, feature: "feature_1", variable: "variable_1")
47 | let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1"))
48 | XCTAssertTrue(tweak.value.boolValue)
49 | }
50 |
51 | func testUpdatesValueForTweak_withInteger() throws {
52 | userDefaultsTweakProvider.set(42, feature: "feature_1", variable: "variable_1")
53 | let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1"))
54 | XCTAssertTrue(tweak.value == 42)
55 | }
56 |
57 | func testUpdatesValueForTweak_withFloat() throws {
58 | userDefaultsTweakProvider.set(Float(12.34), feature: "feature_1", variable: "variable_1")
59 | let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1"))
60 | XCTAssertTrue(tweak.value == Float(12.34))
61 | }
62 |
63 | func testUpdatesValueForTweak_withDouble() throws {
64 | userDefaultsTweakProvider.set(Double(23.45), feature: "feature_1", variable: "variable_1")
65 | let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1"))
66 | XCTAssertTrue(tweak.value == Double(23.45))
67 | }
68 |
69 | func testUpdatesValueForTweak_withString() throws {
70 | userDefaultsTweakProvider.set("Hello", feature: "feature_1", variable: "variable_1")
71 | let tweak = try XCTUnwrap(userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1"))
72 | XCTAssertTrue(tweak.value == "Hello")
73 | }
74 |
75 | func testDecryptionClosure() {
76 | XCTAssertNil(userDefaultsTweakProvider.decryptionClosure)
77 |
78 | userDefaultsTweakProvider.decryptionClosure = { tweak in
79 | (tweak.value.stringValue ?? "") + "Decrypted"
80 | }
81 |
82 | let tweak = Tweak(feature: "feature", variable: "variable", value: "topSecret")
83 | let decryptedTweak = userDefaultsTweakProvider.decryptionClosure?(tweak)
84 |
85 | XCTAssertEqual(decryptedTweak?.stringValue, "topSecretDecrypted")
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/xcshareddata/xcschemes/TweakAccessorGenerator.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
35 |
36 |
37 |
38 |
40 |
46 |
47 |
48 |
49 |
50 |
60 |
62 |
68 |
69 |
70 |
71 |
74 |
75 |
78 |
79 |
82 |
83 |
86 |
87 |
88 |
89 |
95 |
97 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/xcshareddata/xcschemes/TweakAccessorGenerator-Release.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
11 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
62 |
63 |
69 |
70 |
71 |
72 |
75 |
76 |
79 |
80 |
83 |
84 |
87 |
88 |
89 |
90 |
96 |
97 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/Example/JustTweak/Assets/ExampleOptimizelyDatafile.json:
--------------------------------------------------------------------------------
1 | {
2 | "accountId": "12345",
3 | "anonymizeIP": false,
4 | "botFiltering": false,
5 | "projectId": "23456",
6 | "revision": "6",
7 | "version": "4",
8 | "experiments": [
9 | {
10 | "key": "my_experiment",
11 | "id": "45678",
12 | "layerId": "34567",
13 | "status": "Running",
14 | "variations": [
15 | {
16 | "id": "56789",
17 | "key": "control",
18 | "variables": [
19 |
20 | ]
21 | },
22 | {
23 | "id": "67890",
24 | "key": "treatment",
25 | "variables": [
26 |
27 | ]
28 | }
29 | ],
30 | "trafficAllocation": [
31 | {
32 | "entityId": "56789",
33 | "endOfRange": 5000
34 | },
35 | {
36 | "entityId": "67890",
37 | "endOfRange": 10000
38 | }
39 | ],
40 | "audienceIds": [
41 |
42 | ],
43 | "forcedVariations": {
44 |
45 | }
46 | }
47 | ],
48 | "featureFlags": [
49 | {
50 | "experimentIds": [
51 |
52 | ],
53 | "id": "56789",
54 | "key": "price_filter",
55 | "rolloutId": "67890",
56 | "variables": [
57 | {
58 | "defaultValue": "100",
59 | "id": "12345",
60 | "key": "min_price",
61 | "type": "integer"
62 | }
63 | ]
64 | }
65 | ],
66 | "events": [
67 | {
68 | "experimentIds": [
69 | "34567"
70 | ],
71 | "id": "56789",
72 | "key": "my_conversion"
73 | }
74 | ],
75 | "audiences": [
76 |
77 | ],
78 | "attributes": [
79 |
80 | ],
81 | "groups": [
82 |
83 | ],
84 | "rollouts": [
85 | {
86 | "experiments": [
87 | {
88 | "audienceIds": [
89 |
90 | ],
91 | "forcedVariations": {
92 |
93 | },
94 | "id": "23456",
95 | "key": "23456",
96 | "layerId": "34567",
97 | "status": "Running",
98 | "trafficAllocation": [
99 | {
100 | "endOfRange": 5000,
101 | "entityId": "45678"
102 | }
103 | ],
104 | "variations": [
105 | {
106 | "featureEnabled": true,
107 | "id": "56789",
108 | "key": "56789",
109 | "variables": [
110 | {
111 | "id": "67890",
112 | "value": "100"
113 | }
114 | ]
115 | }
116 | ]
117 | }
118 | ],
119 | "id": "78901"
120 | }
121 | ],
122 | "variables": [
123 |
124 | ]
125 | }
126 |
--------------------------------------------------------------------------------
/Example/JustTweak.xcodeproj/xcshareddata/xcschemes/JustTweak-Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
61 |
63 |
69 |
70 |
71 |
72 |
73 |
83 |
85 |
91 |
92 |
93 |
94 |
98 |
99 |
100 |
101 |
107 |
109 |
115 |
116 |
117 |
118 |
120 |
121 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/Example/Tests/Core/TweakManagerCacheTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakManagerCacheTests.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | @testable import JustTweak
8 | @testable import JustTweak_Example
9 |
10 | fileprivate struct Constants {
11 | static let featureActiveValue = true
12 | static let feature = "some_feature"
13 | static let variable = "some_variable"
14 | static let experiment = "some_experiment"
15 | }
16 |
17 | class TweakManagerCacheTests: XCTestCase {
18 |
19 | fileprivate var mockTweakProvider: MockTweakProvider!
20 | var tweakManager: TweakManager!
21 |
22 | override func setUp() {
23 | super.setUp()
24 | mockTweakProvider = MockTweakProvider()
25 | tweakManager = TweakManager(tweakProviders: [mockTweakProvider])
26 | tweakManager.useCache = true
27 | }
28 |
29 | override func tearDown() {
30 | mockTweakProvider = nil
31 | tweakManager = nil
32 | super.tearDown()
33 | }
34 |
35 | // MARK: - isFeatureEnabled(feature:)
36 |
37 | func testFeatureEnabled_CacheDisabled() {
38 | testFeatureEnabled(useCache: false)
39 | }
40 |
41 | func testFeatureEnabled_CacheEnabled() {
42 | testFeatureEnabled(useCache: true)
43 | }
44 |
45 | private func testFeatureEnabled(useCache: Bool) {
46 | tweakManager.useCache = useCache
47 | XCTAssertEqual(mockTweakProvider.isFeatureEnabledCallsCounter, 0)
48 | XCTAssertEqual(tweakManager.isFeatureEnabled(Constants.feature), Constants.featureActiveValue)
49 | XCTAssertEqual(mockTweakProvider.isFeatureEnabledCallsCounter, 1)
50 | XCTAssertEqual(tweakManager.isFeatureEnabled(Constants.feature), Constants.featureActiveValue)
51 | XCTAssertEqual(mockTweakProvider.isFeatureEnabledCallsCounter, useCache ? 1 : 2)
52 | tweakManager.resetCache()
53 | XCTAssertEqual(tweakManager.isFeatureEnabled(Constants.feature), Constants.featureActiveValue)
54 | XCTAssertEqual(mockTweakProvider.isFeatureEnabledCallsCounter, useCache ? 2 : 3)
55 | }
56 |
57 | // MARK: - tweakWith(feature:variable:)
58 |
59 | func testTweakFetch_CacheDisabled() throws {
60 | try tweakFetch(useCache: false)
61 | }
62 |
63 | func testTweakFetch_CacheEnabled() throws {
64 | try tweakFetch(useCache: true)
65 | }
66 |
67 | private func tweakFetch(useCache: Bool) throws {
68 | tweakManager.useCache = useCache
69 | XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, 0)
70 | let value = true
71 | tweakManager.set(value, feature: Constants.feature, variable: Constants.variable)
72 | XCTAssertEqual(try XCTUnwrap(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)).value as! Bool, value)
73 | XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, 1)
74 | XCTAssertEqual(try XCTUnwrap(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)).value as! Bool, value)
75 | XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, useCache ? 1 : 2)
76 | tweakManager.set(value, feature: Constants.feature, variable: Constants.variable)
77 | XCTAssertEqual(try XCTUnwrap(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)).value as! Bool, value)
78 | XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, useCache ? 2 : 3)
79 | tweakManager.resetCache()
80 | XCTAssertEqual(try XCTUnwrap(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)).value as! Bool, value)
81 | XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, useCache ? 3 : 4)
82 | }
83 | }
84 |
85 | fileprivate class MockTweakProvider: MutableTweakProvider {
86 |
87 | var decryptionClosure: ((Tweak) -> TweakValue)?
88 | var logClosure: LogClosure?
89 |
90 | var isFeatureEnabledCallsCounter: Int = 0
91 | var tweakWithFeatureVariableCallsCounter: Int = 0
92 |
93 | var featureBackingStore: [String : Bool] = [Constants.feature: Constants.featureActiveValue]
94 | var tweakBackingStore: [String : [String : Tweak]] = [:]
95 |
96 | func isFeatureEnabled(_ feature: String) -> Bool {
97 | isFeatureEnabledCallsCounter += 1
98 | return featureBackingStore[feature] ?? false
99 | }
100 |
101 | func tweakWith(feature: String, variable: String) throws -> Tweak {
102 | tweakWithFeatureVariableCallsCounter += 1
103 | guard let tweak = tweakBackingStore[feature]?[variable] else {
104 | throw TweakError.notFound
105 | }
106 | return tweak
107 | }
108 |
109 | func set(_ value: TweakValue, feature: String, variable: String) {
110 | let tweak = Tweak(feature: feature, variable: variable, value: value)
111 | if let _ = tweakBackingStore[feature] {
112 | tweakBackingStore[feature]?[variable] = tweak
113 | }
114 | else {
115 | tweakBackingStore[feature] = [variable : tweak]
116 | }
117 | }
118 |
119 | func deleteValue(feature: String, variable: String) {
120 | tweakBackingStore[feature]?[variable] = nil
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Example/Tests/Core/TweakManager+PresentationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakManager+PresentationTests.swift
3 | // JustTweak_Tests
4 | //
5 | // Created by Alberto De Bortoli on 02/02/2020.
6 | // Copyright © 2020 Just Eat. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import JustTweak
11 |
12 | class TweakManager_PresentationTests: XCTestCase {
13 |
14 | var tweakManager: TweakManager!
15 | let localTweakProviderLowPriority: LocalTweakProvider = {
16 | let bundle = Bundle(for: TweakManagerTests.self)
17 | let jsonConfigurationURL = bundle.url(forResource: "LocalTweaks_test", withExtension: "json")!
18 | return LocalTweakProvider(jsonURL: jsonConfigurationURL)
19 | }()
20 | let localTweakProviderHighPriority: LocalTweakProvider = {
21 | let bundle = Bundle(for: TweakManagerTests.self)
22 | let jsonConfigurationURL = bundle.url(forResource: "LocalTweaks_test_override", withExtension: "json")!
23 | return LocalTweakProvider(jsonURL: jsonConfigurationURL)
24 | }()
25 |
26 | func test_GivenOneLocalTweakProvider_WhenFetchedDisplayableTweaks_ThenAllTweaksSortedByTitleAreReturned() {
27 | let tweakProviders: [TweakProvider] = [localTweakProviderLowPriority]
28 | tweakManager = TweakManager(tweakProviders: tweakProviders)
29 | let displayableTweaks = tweakManager.displayableTweaks
30 | let targetTweaks = [
31 | Tweak(feature: "ui_customization",
32 | variable: "display_green_view",
33 | value: true,
34 | title: "Display Green View",
35 | group: "UI Customization"),
36 | Tweak(feature: "ui_customization",
37 | variable: "display_red_view",
38 | value: true,
39 | title: "Display Red View",
40 | group: "UI Customization"),
41 | Tweak(feature: "ui_customization",
42 | variable: "display_yellow_view",
43 | value: false,
44 | title: "Display Yellow View",
45 | group: "UI Customization"),
46 | Tweak(feature: "general",
47 | variable: "greet_on_app_did_become_active",
48 | value: false,
49 | title: "Greet on app launch",
50 | group: "General"),
51 | Tweak(feature: "ui_customization",
52 | variable: "label_text",
53 | value: "Test value",
54 | title: "Label Text",
55 | group: "UI Customization"),
56 | Tweak(feature: "ui_customization",
57 | variable: "red_view_alpha_component",
58 | value: 1.0,
59 | title: "Red View Alpha Component",
60 | group: "UI Customization"),
61 | Tweak(feature: "general",
62 | variable: "tap_to_change_color_enabled",
63 | value: true,
64 | title: "Tap to change views color",
65 | group: "General")
66 | ]
67 | XCTAssertEqual(displayableTweaks, targetTweaks)
68 | }
69 |
70 | func test_GivenTwoLocalTweakProviders_WhenFetchedDisplayableTweaks_ThenTweaksFromBothConfigurationsSortedByTitleAreReturned() {
71 | let tweakProviders: [LocalTweakProvider] = [localTweakProviderHighPriority, localTweakProviderLowPriority]
72 | tweakManager = TweakManager(tweakProviders: tweakProviders)
73 | let displayableTweaks = tweakManager.displayableTweaks
74 | let targetTweaks = [
75 | Tweak(feature: "ui_customization",
76 | variable: "display_blue_view",
77 | value: true,
78 | title: "Display Blue View",
79 | group: "UI Customization"),
80 | Tweak(feature: "ui_customization",
81 | variable: "display_green_view",
82 | value: true,
83 | title: "Display Green View",
84 | group: "UI Customization"),
85 | Tweak(feature: "ui_customization",
86 | variable: "display_red_view",
87 | value: true,
88 | title: "Display Red View",
89 | group: "UI Customization"),
90 | Tweak(feature: "ui_customization",
91 | variable: "display_yellow_view",
92 | value: true,
93 | title: "Display Yellow View",
94 | group: "UI Customization"),
95 | Tweak(feature: "general",
96 | variable: "greet_on_app_did_become_active",
97 | value: false,
98 | title: "Greet on app launch",
99 | group: "General"),
100 | Tweak(feature: "ui_customization",
101 | variable: "label_text",
102 | value: "Overridden value",
103 | title: "Label Text",
104 | group: "UI Customization"),
105 | Tweak(feature: "ui_customization",
106 | variable: "red_view_alpha_component",
107 | value: 1.0,
108 | title: "Red View Alpha Component",
109 | group: "UI Customization"),
110 | Tweak(feature: "general",
111 | variable: "tap_to_change_color_enabled",
112 | value: false,
113 | title: "Tap to change views color",
114 | group: "General")
115 | ]
116 | XCTAssertEqual(displayableTweaks, targetTweaks)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/Example/Tests/Core/TweakTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakTests.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | import JustTweak
8 |
9 | class TweakTests: XCTestCase {
10 |
11 | func testTweakIsEqualToOtherTweakWithSameAttributes() {
12 | let tweakA = Tweak(feature: "some_feature", variable: "some_variable", value: false, title: nil, group: nil)
13 | let tweakB = Tweak(feature: "some_feature", variable: "some_variable", value: false, title: nil, group: nil)
14 | XCTAssertEqual(tweakA, tweakB)
15 | }
16 |
17 | func testTweakIsNotEqualToOtherTweakWithSameIdentifierButDifferentAttributes() {
18 | let tweakA = Tweak(feature: "some_feature", variable: "some_variable", value: false, title: nil, group: nil)
19 | let tweakB = Tweak(feature: "some_feature", variable: "some_variable", value: true, title: nil, group: nil)
20 | XCTAssertNotEqual(tweakA, tweakB)
21 | }
22 |
23 | func testStringTweakIsEqualToOtherTweakWithSameAttributes() {
24 | let tweakA = Tweak(feature: "some_feature", variable: "some_variable", value: "Test", title: nil, group: nil)
25 | let tweakB = Tweak(feature: "some_feature", variable: "some_variable", value: "Test", title: nil, group: nil)
26 | XCTAssertEqual(tweakA, tweakB)
27 | }
28 |
29 | func testIntTweakIsEqualToOtherTweakWithSameAttributes() {
30 | let tweakA = Tweak(feature: "some_feature", variable: "some_variable", value: 1, title: nil, group: nil)
31 | let tweakB = Tweak(feature: "some_feature", variable: "some_variable", value: 1, title: nil, group: nil)
32 | XCTAssertEqual(tweakA, tweakB)
33 | }
34 |
35 | func testFloatTweakIsEqualToOtherTweakWithSameAttributes() {
36 | let value: Float = 1.0
37 | let tweakA = Tweak(feature: "some_feature", variable: "some_variable", value: value, title: nil, group: nil)
38 | let tweakB = Tweak(feature: "some_feature", variable: "some_variable", value: value, title: nil, group: nil)
39 | XCTAssertEqual(tweakA, tweakB)
40 | }
41 |
42 | func testDoubleTweakIsEqualToOtherTweakWithSameAttributes() {
43 | let value: Double = 1.0
44 | let tweakA = Tweak(feature: "some_feature", variable: "some_variable", value: value, title: nil, group: nil)
45 | let tweakB = Tweak(feature: "some_feature", variable: "some_variable", value: value, title: nil, group: nil)
46 | XCTAssertEqual(tweakA, tweakB)
47 | }
48 |
49 | func testMethodsToBrigeValuesToObjectiveC_Bool() {
50 | let value: Double = 1.0
51 | let tweakA = Tweak(feature: "some_feature", variable: "some_variable", value: value, title: nil, group: nil)
52 | XCTAssertFalse(tweakA.boolValue)
53 | let tweakB = Tweak(feature: "some_feature", variable: "some_variable", value: true, title: nil, group: nil)
54 | XCTAssertTrue(tweakB.boolValue)
55 | }
56 |
57 | func testMethodsToBrigeValuesToObjectiveC_Int() {
58 | let value: Int = 1
59 | let tweak = Tweak(feature: "some_feature", variable: "some_variable", value: value, title: nil, group: nil)
60 | XCTAssertEqual(tweak.intValue, value)
61 | }
62 |
63 | func testMethodsToBrigeValuesToObjectiveC_Float() {
64 | let value: Float = 1.5
65 | let tweak = Tweak(feature: "some_feature", variable: "some_variable", value: value, title: nil, group: nil)
66 | XCTAssertEqual(tweak.floatValue, value)
67 | }
68 |
69 | func testMethodsToBrigeValuesToObjectiveC_Double() {
70 | let value: Double = 3.66
71 | let tweak = Tweak(feature: "some_feature", variable: "some_variable", value: value, title: nil, group: nil)
72 | XCTAssertEqual(tweak.doubleValue, value)
73 | }
74 |
75 | func testMethodsToBrigeValuesToObjectiveC_String() {
76 | let value: String = "3.66"
77 | let tweak = Tweak(feature: "some_feature", variable: "some_variable", value: value, title: nil, group: nil)
78 | XCTAssertEqual(tweak.stringValue, value)
79 | }
80 |
81 | func testMethodsToBridgeValuesToObjectiveC_ReturnCorrectValues_FromNumber() {
82 | let tweak = Tweak(feature: "some_feature",
83 | variable: "some_variable",
84 | value: 3.5,
85 | title: nil,
86 | group: nil)
87 | XCTAssertEqual(tweak.doubleValue, 3.5)
88 | XCTAssertEqual(tweak.floatValue, 3.5)
89 | XCTAssertEqual(tweak.intValue, 3)
90 | XCTAssertEqual(tweak.boolValue, false)
91 | XCTAssertNil(tweak.stringValue)
92 | }
93 |
94 | func testMethodsToBridgeValuesToObjectiveC_ReturnCorrectValues_FromBool() {
95 | let tweak = Tweak(feature: "some_feature",
96 | variable: "some_variable",
97 | value: true,
98 | title: nil,
99 | group: nil)
100 | XCTAssertEqual(tweak.doubleValue, 0.0)
101 | XCTAssertEqual(tweak.floatValue, 0.0)
102 | XCTAssertEqual(tweak.intValue, 0)
103 | XCTAssertEqual(tweak.boolValue, true)
104 | XCTAssertNil(tweak.stringValue)
105 | }
106 |
107 | func testMethodsToBridgeValuesToObjectiveC_ReturnCorrectValues_FromString() {
108 | let tweak = Tweak(feature: "some_feature",
109 | variable: "some_variable",
110 | value: "Hello",
111 | title: nil,
112 | group: nil)
113 | XCTAssertEqual(tweak.doubleValue, 0.0)
114 | XCTAssertEqual(tweak.floatValue, 0.0)
115 | XCTAssertEqual(tweak.intValue, 0)
116 | XCTAssertEqual(tweak.boolValue, false)
117 | XCTAssertEqual(tweak.stringValue, "Hello")
118 | }
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/Example/Tests/Core/PropertyWrappersTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Accessor.swift
3 | // Copyright (c) 2019 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | import JustTweak
8 |
9 | class Accessor {
10 |
11 | static let tweakManager: TweakManager = {
12 | let ephemeralTweakProvider: MutableTweakProvider = NSMutableDictionary()
13 | return TweakManager(tweakProviders: [ephemeralTweakProvider])
14 | }()
15 |
16 | @FallbackTweakProperty(fallbackValue: "default",
17 | feature: "stringValue_feature",
18 | variable: "stringValue_variable",
19 | tweakManager: tweakManager)
20 | var stringValue: String
21 |
22 | @FallbackTweakProperty(fallbackValue: false,
23 | feature: "boolValue_feature",
24 | variable: "boolValue_variable",
25 | tweakManager: tweakManager)
26 | var boolValue: Bool
27 |
28 | @FallbackTweakProperty(fallbackValue: 42,
29 | feature: "intValue_feature",
30 | variable: "intValue_variable",
31 | tweakManager: tweakManager)
32 | var intValue: Int
33 |
34 | @FallbackTweakProperty(fallbackValue: 3.14,
35 | feature: "doubleValue_feature",
36 | variable: "doubleValue_variable",
37 | tweakManager: tweakManager)
38 | var doubleValue: Double
39 |
40 | @FallbackTweakProperty(fallbackValue: 108.0,
41 | feature: "floatValue_feature",
42 | variable: "floatValue_variable",
43 | tweakManager: tweakManager)
44 | var floatValue: Float
45 |
46 | @OptionalTweakProperty(fallbackValue: nil,
47 | feature: "optional_stringValue_feature",
48 | variable: "optional_stringValue_variable",
49 | tweakManager: tweakManager)
50 | var optionalStringValue: String?
51 |
52 | @OptionalTweakProperty(fallbackValue: nil,
53 | feature: "optional_boolValue_feature",
54 | variable: "optional_boolValue_variable",
55 | tweakManager: tweakManager)
56 | var optionalBoolValue: Bool?
57 |
58 | @OptionalTweakProperty(fallbackValue: nil,
59 | feature: "optional_intValue_feature",
60 | variable: "optional_intValue_variable",
61 | tweakManager: tweakManager)
62 | var optionalIntValue: Int?
63 |
64 | @OptionalTweakProperty(fallbackValue: nil,
65 | feature: "optional_doubleValue_feature",
66 | variable: "optional_doubleValue_variable",
67 | tweakManager: tweakManager)
68 | var optionalDoubleValue: Double?
69 |
70 | @OptionalTweakProperty(fallbackValue: nil,
71 | feature: "optional_floatValue_feature",
72 | variable: "optional_floatValue_variable",
73 | tweakManager: tweakManager)
74 | var optionalFloatValue: Float?
75 | }
76 |
77 | class FeatureFlagPropertyWrapperTests: XCTestCase {
78 |
79 | var accessor: Accessor!
80 |
81 | override func setUp() {
82 | accessor = Accessor()
83 | }
84 |
85 | override func tearDown() {
86 | accessor = nil
87 | }
88 |
89 | func test_StringFeatureFlag() {
90 | XCTAssertEqual(accessor.stringValue, "default")
91 | let newValue = "other value"
92 | accessor.stringValue = newValue
93 | XCTAssertEqual(accessor.stringValue, newValue)
94 | }
95 |
96 | func test_BoolFeatureFlag() {
97 | XCTAssertFalse(accessor.boolValue)
98 | accessor.boolValue = true
99 | XCTAssertTrue(accessor.boolValue)
100 | }
101 |
102 | func test_IntFeatureFlag() {
103 | XCTAssertEqual(accessor.intValue, 42)
104 | let newValue = 100
105 | accessor.intValue = newValue
106 | XCTAssertEqual(accessor.intValue, newValue)
107 | }
108 |
109 | func test_DoubleFeatureFlag() {
110 | XCTAssertEqual(accessor.doubleValue, 3.14)
111 | let newValue = 100.200
112 | accessor.doubleValue = newValue
113 | XCTAssertEqual(accessor.doubleValue, newValue)
114 | }
115 |
116 | func test_FloatFeatureFlag() {
117 | XCTAssertEqual(accessor.floatValue, 108.0)
118 | let newValue: Float = 100.200
119 | accessor.floatValue = newValue
120 | XCTAssertEqual(accessor.floatValue, newValue)
121 | }
122 |
123 | func test_OptionalStringFeatureFlag() {
124 | XCTAssertEqual(accessor.optionalStringValue, nil)
125 | let newValue = "some value"
126 | accessor.optionalStringValue = newValue
127 | XCTAssertEqual(accessor.optionalStringValue, newValue)
128 | }
129 |
130 | func test_OptionalBoolFeatureFlag() {
131 | XCTAssertEqual(accessor.optionalBoolValue, nil)
132 | accessor.optionalBoolValue = true
133 | XCTAssertEqual(accessor.optionalBoolValue, true)
134 | }
135 |
136 | func test_OptionalIntFeatureFlag() {
137 | XCTAssertEqual(accessor.optionalIntValue, nil)
138 | let newValue = 100
139 | accessor.optionalIntValue = newValue
140 | XCTAssertEqual(accessor.optionalIntValue, newValue)
141 | }
142 |
143 | func test_OptionalDoubleFeatureFlag() {
144 | XCTAssertEqual(accessor.optionalDoubleValue, nil)
145 | let newValue = 3.14
146 | accessor.optionalDoubleValue = newValue
147 | XCTAssertEqual(accessor.optionalDoubleValue, newValue)
148 | }
149 |
150 | func test_OptionalFloatFeatureFlag() {
151 | XCTAssertEqual(accessor.optionalFloatValue, nil)
152 | let newValue: Float = 108.0
153 | accessor.optionalFloatValue = newValue
154 | XCTAssertEqual(accessor.optionalFloatValue, newValue)
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/Example/Tests/Core/TweakManagerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakManagerTests.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | @testable import JustTweak
8 |
9 | class TweakManagerTests: XCTestCase {
10 |
11 | var tweakManager: TweakManager!
12 | let localTweakProvider: TweakProvider = {
13 | let bundle = Bundle(for: TweakManagerTests.self)
14 | let jsonConfigurationURL = bundle.url(forResource: "LocalTweaks_test", withExtension: "json")!
15 | return LocalTweakProvider(jsonURL: jsonConfigurationURL)
16 | }()
17 | var userDefaultsTweakProvider: UserDefaultsTweakProvider!
18 |
19 | override func setUp() {
20 | super.setUp()
21 | let mockTweakProvider = MockTweakProvider()
22 | let testUserDefaults = UserDefaults(suiteName: "com.JustTweak.TweakManagerTests")!
23 | userDefaultsTweakProvider = UserDefaultsTweakProvider(userDefaults: testUserDefaults)
24 | let tweakProviders: [TweakProvider] = [userDefaultsTweakProvider, mockTweakProvider, localTweakProvider]
25 | tweakManager = TweakManager(tweakProviders: tweakProviders)
26 | }
27 |
28 | override func tearDown() {
29 | userDefaultsTweakProvider.deleteValue(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)
30 | tweakManager = nil
31 | super.tearDown()
32 | }
33 |
34 | func testReturnsNoMutableTweakProvider_IfNoneHasBeenPassedToInitializer() {
35 | let tweakManager = TweakManager(tweakProviders: [localTweakProvider])
36 | XCTAssertNil(tweakManager.mutableTweakProvider)
37 | }
38 |
39 | func testReturnsNil_ForUndefinedTweak() {
40 | XCTAssertNil(try? tweakManager.tweakWith(feature: Features.uiCustomization, variable: "some_undefined_tweak"))
41 | }
42 |
43 | func testReturnsRemoteConfigValue_ForDisplayRedViewTweak() throws {
44 | XCTAssertTrue(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView)).boolValue)
45 | }
46 |
47 | func testReturnsRemoteConfigValue_ForDisplayYellowViewTweak() throws {
48 | XCTAssertFalse(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView)).boolValue)
49 | }
50 |
51 | func testReturnsRemoteConfigValue_ForDisplayGreenViewTweak() throws {
52 | XCTAssertFalse(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayGreenView)).boolValue)
53 | }
54 |
55 | func testReturnsRemoteConfigValue_ForGreetOnAppDidBecomeActiveTweak() throws {
56 | XCTAssertTrue(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)).boolValue)
57 | }
58 |
59 | func testReturnsJSONConfigValue_ForTapToChangeViewColorTweak_AsYetUnkown() throws {
60 | XCTAssertTrue(try XCTUnwrap(tweakManager.tweakWith(feature: Features.general, variable: Variables.tapToChangeViewColor)).boolValue)
61 | }
62 |
63 | func testReturnsUserSetValue_ForGreetOnAppDidBecomeActiveTweak_AfterUpdatingUserDefaultsTweakProvider() throws {
64 | let mutableTweakProvider = tweakManager.mutableTweakProvider!
65 | mutableTweakProvider.set(false, feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)
66 | XCTAssertFalse(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)).boolValue)
67 | }
68 |
69 | func testCallsClosureForRegisteredObserverWhenAnyConfigurationChanges() {
70 | var didCallClosure = false
71 | tweakManager.registerForConfigurationsUpdates(self) { tweakIdentifier in
72 | didCallClosure = true
73 | }
74 | let tweak = Tweak(feature: "feature", variable: "variable", value: "value")
75 | let userInfo = [TweakProviderDidChangeNotificationTweakKey: tweak]
76 | NotificationCenter.default.post(name: TweakProviderDidChangeNotification, object: self, userInfo: userInfo)
77 | XCTAssertTrue(didCallClosure)
78 | }
79 |
80 | func testDoesNotCallClosureForDeregisteredObserverWhenAnyConfigurationChanges() {
81 | var didCallClosure = false
82 | tweakManager.registerForConfigurationsUpdates(self) { tweakIdentifier in
83 | didCallClosure = true
84 | }
85 | tweakManager.deregisterFromConfigurationsUpdates(self)
86 | let tweak = Tweak(feature: "feature", variable: "variable", value: "value")
87 | let userInfo = [TweakProviderDidChangeNotificationTweakKey: tweak]
88 | NotificationCenter.default.post(name: TweakProviderDidChangeNotification, object: self, userInfo: userInfo)
89 | XCTAssertFalse(didCallClosure)
90 | }
91 |
92 | func testTweakManagerDecryption() throws {
93 | let url = try XCTUnwrap(Bundle.main.url(forResource: "LocalTweaks_example", withExtension: "json"))
94 |
95 | tweakManager.tweakProviders.append(LocalTweakProvider(jsonURL: url))
96 |
97 | tweakManager.decryptionClosure = { tweak in
98 | String((tweak.value.stringValue ?? "").reversed())
99 | }
100 |
101 | let feature = "general"
102 | let variable = "encrypted_answer_to_the_universe"
103 | let tweak = try? tweakManager.tweakWith(feature: feature, variable: variable)
104 |
105 | XCTAssertEqual("Definitely not 42", tweak?.stringValue)
106 | }
107 |
108 | func testSetTweakManagerDecryptionClosureThenDecryptionClosureIsSetForProviders() throws {
109 | let mutableTweakProvider = try XCTUnwrap(tweakManager.mutableTweakProvider)
110 |
111 | XCTAssertNil(mutableTweakProvider.decryptionClosure)
112 |
113 | tweakManager.decryptionClosure = { tweak in
114 | tweak.value
115 | }
116 |
117 | XCTAssertNotNil(mutableTweakProvider.decryptionClosure)
118 | }
119 | }
120 |
121 | fileprivate class MockTweakProvider: TweakProvider {
122 |
123 | var logClosure: LogClosure?
124 | var decryptionClosure: ((Tweak) -> TweakValue)?
125 | let features: [String : [String]] = [:]
126 | let knownValues = [Variables.displayRedView: ["Value": true],
127 | Variables.displayYellowView: ["Value": false],
128 | Variables.displayGreenView: ["Value": false],
129 | Variables.greetOnAppDidBecomeActive: ["Value": true]]
130 |
131 | func isFeatureEnabled(_ feature: String) -> Bool {
132 | return false
133 | }
134 |
135 | func tweakWith(feature: String, variable: String) throws -> Tweak {
136 | guard let value = knownValues[variable] else { throw TweakError.notFound }
137 | return Tweak(feature: feature, variable: variable, value: value["Value"]!, title: nil, group: nil)
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakAccessorCodeGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakAccessorCodeGenerator.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | class TweakAccessorCodeGenerator {
9 |
10 | private let featuresConst = "Features"
11 | private let variablesConst = "Variables"
12 |
13 | private let featureConstantsConst = ""
14 | private let variableConstantsConst = ""
15 | private let classContentConst = ""
16 | private let tweakManagerConst = ""
17 | }
18 |
19 | extension TweakAccessorCodeGenerator {
20 |
21 | func generateConstantsFileContent(tweaks: [Tweak],
22 | configuration: Configuration) -> String {
23 | let template = self.constantsTemplate(with: configuration.accessorName)
24 | let featureConstants = self.featureConstantsCodeBlock(with: tweaks)
25 | let variableConstants = self.variableConstantsCodeBlock(with: tweaks)
26 |
27 | let content = template
28 | .replacingOccurrences(of: featureConstantsConst, with: featureConstants)
29 | .replacingOccurrences(of: variableConstantsConst, with: variableConstants)
30 | .trimmingCharacters(in: .whitespacesAndNewlines) + "\n"
31 | return content
32 | }
33 |
34 | func generateAccessorFileContent(tweaksFilename: String,
35 | tweaks: [Tweak],
36 | configuration: Configuration) -> String {
37 | let template = self.accessorTemplate(with: configuration.accessorName)
38 | let tweakManager = self.tweakManagerCodeBlock()
39 | let classContent = self.classContent(with: tweaks, configuration: configuration)
40 |
41 | let content = template
42 | .replacingOccurrences(of: tweakManagerConst, with: tweakManager)
43 | .replacingOccurrences(of: classContentConst, with: classContent)
44 | .trimmingCharacters(in: .whitespacesAndNewlines) + "\n"
45 | return content
46 | }
47 |
48 | func constantsTemplate(with className: String) -> String {
49 | """
50 | // \(className)+Constants.swift
51 |
52 | /// Generated by TweakAccessorGenerator
53 |
54 | import Foundation
55 |
56 | extension \(className) {
57 |
58 | \(featureConstantsConst)
59 |
60 | \(variableConstantsConst)
61 | }
62 | """
63 | }
64 |
65 | private func accessorTemplate(with className: String) -> String {
66 | """
67 | // \(className).swift
68 |
69 | /// Generated by TweakAccessorGenerator
70 |
71 | import Foundation
72 | import JustTweak
73 |
74 | class \(className) {
75 |
76 | \(tweakManagerConst)
77 |
78 | \(classContentConst)
79 | }
80 | """
81 | }
82 |
83 | private func featureConstantsCodeBlock(with tweaks: [Tweak]) -> String {
84 | var features = Set()
85 | for tweak in tweaks {
86 | features.insert(tweak.feature)
87 | }
88 | let content: [String] = features.map {
89 | """
90 | static let \($0.camelCased()) = "\($0)"
91 | """
92 | }
93 | return """
94 | struct \(featuresConst) {
95 | \(content.sorted().joined(separator: "\n"))
96 | }
97 | """
98 | }
99 |
100 | private func variableConstantsCodeBlock(with tweaks: [Tweak]) -> String {
101 | var variables = Set()
102 | for tweak in tweaks {
103 | variables.insert(tweak.variable)
104 | }
105 | let content: [String] = variables.map {
106 | """
107 | static let \($0.camelCased()) = "\($0)"
108 | """
109 | }
110 | return """
111 | struct \(variablesConst) {
112 | \(content.sorted().joined(separator: "\n"))
113 | }
114 | """
115 | }
116 |
117 | private func tweakManagerCodeBlock() -> String {
118 | """
119 | private(set) var tweakManager: TweakManager
120 |
121 | init(with tweakManager: TweakManager) {
122 | self.tweakManager = tweakManager
123 | }
124 | """
125 | }
126 |
127 | private func classContent(with tweaks: [Tweak], configuration: Configuration) -> String {
128 | var content: Set = []
129 | tweaks.forEach {
130 | content.insert(tweakComputedProperty(for: $0))
131 | }
132 | return content.sorted().joined(separator: "\n\n")
133 | }
134 |
135 | private func tweakPropertyWrapper(for tweak: Tweak) -> String {
136 | let propertyName = tweak.propertyName ?? tweak.variable.camelCased()
137 | return """
138 | @TweakProperty(feature: \(featuresConst).\(tweak.feature.camelCased()),
139 | variable: \(variablesConst).\(tweak.variable.camelCased()),
140 | tweakManager: tweakManager)
141 | var \(propertyName): \(tweak.valueType)
142 | """
143 | }
144 |
145 | private func tweakComputedProperty(for tweak: Tweak) -> String {
146 | let propertyName = tweak.propertyName ?? tweak.variable.camelCased()
147 | let feature = "\(featuresConst).\(tweak.feature.camelCased())"
148 | let variable = "\(variablesConst).\(tweak.variable.camelCased())"
149 | let castProperty = try! self.castProperty(for: tweak.valueType)
150 | let defaultValue = try! self.defaultValue(for: tweak.valueType)
151 | return """
152 | var \(propertyName): \(tweak.valueType) {
153 | get { (try? tweakManager.tweakWith(feature: \(feature), variable: \(variable)))?.\(castProperty) ?? \(defaultValue) }
154 | set { tweakManager.set(newValue, feature: \(feature), variable: \(variable)) }
155 | }
156 | """
157 | }
158 |
159 | private func castProperty(for valueType: String) throws -> String {
160 | switch valueType {
161 | case "String":
162 | return "stringValue"
163 | case "Bool":
164 | return "boolValue"
165 | case "Double":
166 | return "doubleValue"
167 | case "Int":
168 | return "intValue"
169 | default:
170 | throw "Unsupported value type '\(valueType)'"
171 | }
172 | }
173 |
174 | private func defaultValue(for valueType: String) throws -> String {
175 | switch valueType {
176 | case "String":
177 | return "\"\""
178 | case "Bool":
179 | return "false"
180 | case "Double":
181 | return "0.0"
182 | case "Int":
183 | return "0"
184 | default:
185 | throw "Unsupported value type '\(valueType)'"
186 | }
187 | }
188 |
189 | private func formatCustomTweakProviderSetupCode(_ setupCode: String) -> String {
190 | setupCode.split(separator: "\n")
191 | .map { " " + $0 }
192 | .joined(separator: "\n")
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/Framework/Sources/TweakManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakManager.swift
3 | // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | final public class TweakManager {
9 |
10 | var tweakProviders: [TweakProvider]
11 |
12 | public var logClosure: LogClosure? {
13 | didSet {
14 | for (index, _) in tweakProviders.enumerated() {
15 | tweakProviders[index].logClosure = logClosure
16 | }
17 | }
18 | }
19 |
20 | public var decryptionClosure: ((Tweak) -> TweakValue)? {
21 | didSet {
22 | for (index, _) in tweakProviders.enumerated() {
23 | tweakProviders[index].decryptionClosure = decryptionClosure
24 | }
25 | }
26 | }
27 |
28 | public var useCache: Bool = false {
29 | didSet {
30 | if useCache != oldValue {
31 | resetCache()
32 | }
33 | }
34 | }
35 |
36 | private let queue = DispatchQueue(label: "com.justeat.tweakManager")
37 |
38 | private var featureCache = [String : Bool]()
39 | private var tweakCache = [String : [String : Tweak]]()
40 | private var experimentCache = [String : String]()
41 | private var observersMap = [NSObject : NSObjectProtocol]()
42 |
43 | var mutableTweakProvider: MutableTweakProvider? {
44 | return tweakProviders.first { $0 is MutableTweakProvider } as? MutableTweakProvider
45 | }
46 |
47 | public init(tweakProviders: [TweakProvider]) {
48 | self.tweakProviders = tweakProviders
49 | for (index, _) in self.tweakProviders.enumerated() {
50 | self.tweakProviders[index].logClosure = logClosure
51 | }
52 | let notificationCenter = NotificationCenter.default
53 | notificationCenter.addObserver(self, selector: #selector(configurationDidChange), name: TweakProviderDidChangeNotification, object: nil)
54 | }
55 |
56 | deinit {
57 | NotificationCenter.default.removeObserver(self)
58 | }
59 | }
60 |
61 | extension TweakManager: MutableTweakProvider {
62 |
63 | public func isFeatureEnabled(_ feature: String) -> Bool {
64 | queue.sync {
65 | if useCache, let cachedFeature = featureCache[feature] {
66 | logClosure?("Feature '\(cachedFeature)' found in cache.)", .verbose)
67 | return cachedFeature
68 | }
69 |
70 | var enabled = false
71 | for (_, configuration) in tweakProviders.enumerated() {
72 | if configuration.isFeatureEnabled(feature) {
73 | enabled = true
74 | break
75 | }
76 | }
77 | if useCache {
78 | featureCache[feature] = enabled
79 | }
80 | return enabled
81 | }
82 | }
83 |
84 | public func tweakWith(feature: String, variable: String) throws -> Tweak {
85 | try queue.sync {
86 | if useCache, let cachedTweaks = tweakCache[feature], let cachedTweak = cachedTweaks[variable] {
87 | logClosure?("Tweak '\(cachedTweak)' found in cache.)", .verbose)
88 | return cachedTweak
89 | }
90 |
91 | var result: Tweak? = nil
92 | for (_, tweakProvider) in tweakProviders.enumerated() {
93 | if let tweak = try? tweakProvider.tweakWith(feature: feature, variable: variable) {
94 | logClosure?("Tweak '\(tweak)' found in configuration \(tweakProvider))", .verbose)
95 |
96 | result = Tweak(feature: feature,
97 | variable: variable,
98 | value: tweak.value,
99 | title: tweak.title,
100 | group: tweak.group,
101 | source: "\(type(of: tweakProvider))")
102 | break
103 | } else {
104 | let logMessage = "Tweak with identifier '\(variable)' in configuration \(tweakProvider)) could NOT be found or has an invalid configuration"
105 | logClosure?(logMessage, .verbose)
106 | }
107 | }
108 | guard let result = result else {
109 | logClosure?("No Tweak found for identifier '\(variable)'", .verbose)
110 | throw TweakError.notFound
111 | }
112 | logClosure?("Tweak with feature '\(feature)' and variable '\(variable)' resolved. Using '\(result)'.", .debug)
113 | if useCache {
114 | if let _ = tweakCache[feature] {
115 | tweakCache[feature]?[variable] = result
116 | } else {
117 | tweakCache[feature] = [variable : result]
118 | }
119 | }
120 | return result
121 | }
122 | }
123 |
124 | public func set(_ value: TweakValue, feature: String, variable: String) {
125 | guard let mutableTweakProvider = self.mutableTweakProvider else { return }
126 | if useCache {
127 | queue.sync {
128 | // cannot use write-through cache because tweakWith(feature:variable:) returns a Tweak, but here we only have a TweakValue
129 | // we simply set the entry to nil so the next fetch will go through the list of configurations and subsequently re-cache
130 | tweakCache[feature]?[variable] = nil
131 | }
132 | }
133 | mutableTweakProvider.set(value, feature: feature, variable: variable)
134 | }
135 |
136 | public func deleteValue(feature: String, variable: String) {
137 | guard let mutableTweakProvider = self.mutableTweakProvider else { return }
138 | if useCache {
139 | queue.sync {
140 | tweakCache[feature]?[variable] = nil
141 | }
142 | }
143 | mutableTweakProvider.deleteValue(feature: feature, variable: variable)
144 | }
145 | }
146 |
147 | extension TweakManager {
148 |
149 | public func registerForConfigurationsUpdates(_ object: NSObject, closure: @escaping (Tweak) -> Void) {
150 | self.deregisterFromConfigurationsUpdates(object)
151 | queue.sync {
152 | let queue = OperationQueue.main
153 | let name = TweakProviderDidChangeNotification
154 | let notificationsCenter = NotificationCenter.default
155 | let observer = notificationsCenter.addObserver(forName: name, object: nil, queue: queue) { notification in
156 | guard let tweak = notification.userInfo?[TweakProviderDidChangeNotificationTweakKey] as? Tweak else { return }
157 | closure(tweak)
158 | }
159 | observersMap[object] = observer
160 | }
161 | }
162 |
163 | public func deregisterFromConfigurationsUpdates(_ object: NSObject) {
164 | queue.sync {
165 | guard let observer = observersMap[object] else { return }
166 | NotificationCenter.default.removeObserver(observer)
167 | observersMap.removeValue(forKey: object)
168 | }
169 | }
170 |
171 | @objc private func configurationDidChange() {
172 | if useCache {
173 | resetCache()
174 | }
175 | }
176 | }
177 |
178 | extension TweakManager {
179 |
180 | public func resetCache() {
181 | queue.sync {
182 | featureCache = [String : Bool]()
183 | tweakCache = [String : [String : Tweak]]()
184 | experimentCache = [String : String]()
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/Example/Tests/UI/TweakViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakViewControllerTests.swift
3 | // Copyright (c) 2019 Just Eat Holding Ltd. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | @testable import JustTweak
8 |
9 | class TweakViewControllerTests: XCTestCase {
10 |
11 | private var rootWindow: UIWindow!
12 | private var viewController: TweakViewController!
13 | private var tweakManager: TweakManager!
14 |
15 | override func setUp() {
16 | super.setUp()
17 | let bundle = Bundle(for: TweakViewControllerTests.self)
18 | let jsonURL = bundle.url(forResource: "LocalTweaks_test", withExtension: "json")
19 | let localTweakProvider = LocalTweakProvider(jsonURL: jsonURL!)
20 | let userDefaults = UserDefaults(suiteName: "com.JustTweaks.Tests\(NSDate.timeIntervalSinceReferenceDate)")!
21 | let userDefaultsTweakProvider = UserDefaultsTweakProvider(userDefaults: userDefaults)
22 | let tweakProviders: [TweakProvider] = [userDefaultsTweakProvider, localTweakProvider]
23 | tweakManager = TweakManager(tweakProviders: tweakProviders)
24 | viewController = TweakViewController(style: .plain, tweakManager: tweakManager)
25 |
26 | rootWindow = UIWindow(frame: UIScreen.main.bounds)
27 | rootWindow.makeKeyAndVisible()
28 | rootWindow.isHidden = false
29 | rootWindow.rootViewController = viewController
30 | _ = viewController.view
31 | viewController.viewWillAppear(false)
32 | viewController.viewDidAppear(false)
33 | }
34 |
35 | override func tearDown() {
36 | let mutableTweakProvider = tweakManager.mutableTweakProvider!
37 | mutableTweakProvider.deleteValue(feature: "feature_1", variable: "variable_1")
38 | viewController = nil
39 |
40 | let rootViewController = rootWindow!.rootViewController!
41 | rootViewController.viewWillDisappear(false)
42 | rootViewController.viewDidDisappear(false)
43 | rootWindow.rootViewController = nil
44 | rootWindow.isHidden = true
45 | self.rootWindow = nil
46 | self.viewController = nil
47 |
48 | super.tearDown()
49 | }
50 |
51 | // MARK: Generic Data Display
52 |
53 | func testHasExpectedNumberOfSections() {
54 | XCTAssertEqual(2, viewController.numberOfSections(in: viewController.tableView))
55 | }
56 |
57 | func testGroupedTweaksAreDisplayedInTheirOwnSections() {
58 | XCTAssertEqual(2, viewController.tableView(viewController.tableView, numberOfRowsInSection: 0))
59 | XCTAssertEqual("General", viewController.tableView(viewController.tableView, titleForHeaderInSection: 0))
60 | XCTAssertEqual(5, viewController.tableView(viewController.tableView, numberOfRowsInSection: 1))
61 | XCTAssertEqual("UI Customization", viewController.tableView(viewController.tableView, titleForHeaderInSection: 1))
62 | }
63 |
64 | // MARK: Convenience Methods
65 |
66 | func testReturnsCorrectIndexPathForTweak_WhenTweakFound() {
67 | let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayGreenView)!
68 | let expectedIndexPath = IndexPath(row: 0, section: 1)
69 | XCTAssertEqual(indexPath, expectedIndexPath)
70 | }
71 |
72 | func testReturnsCorrectIndexPathForTweak_WhenTweakFound_2() {
73 | let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayRedView)!
74 | let expectedIndexPath = IndexPath(row: 1, section: 1)
75 | XCTAssertEqual(indexPath, expectedIndexPath)
76 | }
77 |
78 | func testReturnsCorrectIndexPathForTweak_WhenTweakFound_3() {
79 | let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayYellowView)!
80 | let expectedIndexPath = IndexPath(row: 2, section: 1)
81 | XCTAssertEqual(indexPath, expectedIndexPath)
82 | }
83 |
84 | // MARK: Tweak Cells Display
85 |
86 | func testReturnsCorrectIndexPathForTweak_WhenTweakNotFound() {
87 | let indexPath = viewController.indexPathForTweak(with: Features.general, variable: "some_nonexisting_tweak")
88 | XCTAssertNil(indexPath)
89 | }
90 |
91 | func testDisplaysTweakOn_IfEnabled() {
92 | let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayYellowView)!
93 | let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath) as! BooleanTweakTableViewCell
94 | XCTAssertFalse(cell.switchControl.isOn)
95 | }
96 |
97 | func testDisplaysTweakOff_IfDisabled() {
98 | let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayRedView)!
99 | let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath) as! BooleanTweakTableViewCell
100 | XCTAssertTrue(cell.switchControl.isOn)
101 | }
102 |
103 | func testDisplaysTweakTitle_ForTweakThatHaveIt() {
104 | let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayRedView)!
105 | let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath)
106 | XCTAssertEqual(cell.textLabel?.text, "Display Red View")
107 | XCTAssertEqual((cell as! TweakViewControllerCell).title, "Display Red View")
108 | }
109 |
110 | func testDisplaysNumericTweaksCorrectly() {
111 | let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.redViewAlpha)!
112 | let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath) as? NumericTweakTableViewCell
113 | XCTAssertEqual(cell?.title, "Red View Alpha Component")
114 | XCTAssertEqual(cell?.textField.text, "1.0")
115 | }
116 |
117 | func testDisplaysTextTweaksCorrectly() {
118 | let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.labelText)!
119 | let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath) as? TextTweakTableViewCell
120 | XCTAssertEqual(cell?.title, "Label Text")
121 | XCTAssertEqual(cell?.textField.text, "Test value")
122 | }
123 |
124 | // MARK: Cells Actions
125 |
126 | func testUpdatesValueOfTweak_WhenUserTooglesSwitchOnBooleanCell() throws {
127 | viewController.beginAppearanceTransition(true, animated: false)
128 | viewController.endAppearanceTransition()
129 |
130 | let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayYellowView)!
131 | let cell = viewController.tableView.cellForRow(at: indexPath) as! BooleanTweakTableViewCell
132 | cell.switchControl.isOn = true
133 | cell.switchControl.sendActions(for: .valueChanged)
134 | XCTAssertTrue(try XCTUnwrap(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView)).boolValue)
135 | }
136 |
137 | // MARK: Other Actions
138 |
139 | func testAsksToBeDismissedWhenDoneButtonIsTapped() {
140 | class FakeViewController: TweakViewController {
141 | fileprivate let mockPresentingViewController = MockPresentingViewController()
142 | override var presentingViewController: UIViewController? {
143 | get {
144 | return mockPresentingViewController
145 | }
146 | }
147 | }
148 | let vc = FakeViewController(style: .grouped, tweakManager: tweakManager)
149 | vc.dismissViewController()
150 | XCTAssertTrue(vc.mockPresentingViewController.didCallDismissal)
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.5)
5 | rexml
6 | activesupport (5.2.8)
7 | concurrent-ruby (~> 1.0, >= 1.0.2)
8 | i18n (>= 0.7, < 2)
9 | minitest (~> 5.1)
10 | tzinfo (~> 1.1)
11 | addressable (2.8.0)
12 | public_suffix (>= 2.0.2, < 5.0)
13 | algoliasearch (1.27.5)
14 | httpclient (~> 2.8, >= 2.8.3)
15 | json (>= 1.5.1)
16 | artifactory (3.0.15)
17 | atomos (0.1.3)
18 | aws-eventstream (1.2.0)
19 | aws-partitions (1.601.0)
20 | aws-sdk-core (3.131.2)
21 | aws-eventstream (~> 1, >= 1.0.2)
22 | aws-partitions (~> 1, >= 1.525.0)
23 | aws-sigv4 (~> 1.1)
24 | jmespath (~> 1, >= 1.6.1)
25 | aws-sdk-kms (1.57.0)
26 | aws-sdk-core (~> 3, >= 3.127.0)
27 | aws-sigv4 (~> 1.1)
28 | aws-sdk-s3 (1.114.0)
29 | aws-sdk-core (~> 3, >= 3.127.0)
30 | aws-sdk-kms (~> 1)
31 | aws-sigv4 (~> 1.4)
32 | aws-sigv4 (1.5.0)
33 | aws-eventstream (~> 1, >= 1.0.2)
34 | babosa (1.0.4)
35 | claide (1.1.0)
36 | cocoapods (1.10.2)
37 | addressable (~> 2.6)
38 | claide (>= 1.0.2, < 2.0)
39 | cocoapods-core (= 1.10.2)
40 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
41 | cocoapods-downloader (>= 1.4.0, < 2.0)
42 | cocoapods-plugins (>= 1.0.0, < 2.0)
43 | cocoapods-search (>= 1.0.0, < 2.0)
44 | cocoapods-trunk (>= 1.4.0, < 2.0)
45 | cocoapods-try (>= 1.1.0, < 2.0)
46 | colored2 (~> 3.1)
47 | escape (~> 0.0.4)
48 | fourflusher (>= 2.3.0, < 3.0)
49 | gh_inspector (~> 1.0)
50 | molinillo (~> 0.6.6)
51 | nap (~> 1.0)
52 | ruby-macho (~> 1.4)
53 | xcodeproj (>= 1.19.0, < 2.0)
54 | cocoapods-core (1.10.2)
55 | activesupport (> 5.0, < 6)
56 | addressable (~> 2.6)
57 | algoliasearch (~> 1.0)
58 | concurrent-ruby (~> 1.1)
59 | fuzzy_match (~> 2.0.4)
60 | nap (~> 1.0)
61 | netrc (~> 0.11)
62 | public_suffix
63 | typhoeus (~> 1.0)
64 | cocoapods-deintegrate (1.0.5)
65 | cocoapods-downloader (1.6.3)
66 | cocoapods-plugins (1.0.0)
67 | nap
68 | cocoapods-search (1.0.1)
69 | cocoapods-trunk (1.6.0)
70 | nap (>= 0.8, < 2.0)
71 | netrc (~> 0.11)
72 | cocoapods-try (1.2.0)
73 | colored (1.2)
74 | colored2 (3.1.2)
75 | commander (4.6.0)
76 | highline (~> 2.0.0)
77 | concurrent-ruby (1.1.10)
78 | declarative (0.0.20)
79 | digest-crc (0.6.4)
80 | rake (>= 12.0.0, < 14.0.0)
81 | domain_name (0.5.20190701)
82 | unf (>= 0.0.5, < 1.0.0)
83 | dotenv (2.7.6)
84 | emoji_regex (3.2.3)
85 | escape (0.0.4)
86 | ethon (0.15.0)
87 | ffi (>= 1.15.0)
88 | excon (0.92.3)
89 | faraday (1.10.0)
90 | faraday-em_http (~> 1.0)
91 | faraday-em_synchrony (~> 1.0)
92 | faraday-excon (~> 1.1)
93 | faraday-httpclient (~> 1.0)
94 | faraday-multipart (~> 1.0)
95 | faraday-net_http (~> 1.0)
96 | faraday-net_http_persistent (~> 1.0)
97 | faraday-patron (~> 1.0)
98 | faraday-rack (~> 1.0)
99 | faraday-retry (~> 1.0)
100 | ruby2_keywords (>= 0.0.4)
101 | faraday-cookie_jar (0.0.7)
102 | faraday (>= 0.8.0)
103 | http-cookie (~> 1.0.0)
104 | faraday-em_http (1.0.0)
105 | faraday-em_synchrony (1.0.0)
106 | faraday-excon (1.1.0)
107 | faraday-httpclient (1.0.1)
108 | faraday-multipart (1.0.4)
109 | multipart-post (~> 2)
110 | faraday-net_http (1.0.1)
111 | faraday-net_http_persistent (1.2.0)
112 | faraday-patron (1.0.0)
113 | faraday-rack (1.0.0)
114 | faraday-retry (1.0.3)
115 | faraday_middleware (1.2.0)
116 | faraday (~> 1.0)
117 | fastimage (2.2.6)
118 | fastlane (2.191.0)
119 | CFPropertyList (>= 2.3, < 4.0.0)
120 | addressable (>= 2.8, < 3.0.0)
121 | artifactory (~> 3.0)
122 | aws-sdk-s3 (~> 1.0)
123 | babosa (>= 1.0.3, < 2.0.0)
124 | bundler (>= 1.12.0, < 3.0.0)
125 | colored
126 | commander (~> 4.6)
127 | dotenv (>= 2.1.1, < 3.0.0)
128 | emoji_regex (>= 0.1, < 4.0)
129 | excon (>= 0.71.0, < 1.0.0)
130 | faraday (~> 1.0)
131 | faraday-cookie_jar (~> 0.0.6)
132 | faraday_middleware (~> 1.0)
133 | fastimage (>= 2.1.0, < 3.0.0)
134 | gh_inspector (>= 1.1.2, < 2.0.0)
135 | google-apis-androidpublisher_v3 (~> 0.3)
136 | google-apis-playcustomapp_v1 (~> 0.1)
137 | google-cloud-storage (~> 1.31)
138 | highline (~> 2.0)
139 | json (< 3.0.0)
140 | jwt (>= 2.1.0, < 3)
141 | mini_magick (>= 4.9.4, < 5.0.0)
142 | multipart-post (~> 2.0.0)
143 | naturally (~> 2.2)
144 | plist (>= 3.1.0, < 4.0.0)
145 | rubyzip (>= 2.0.0, < 3.0.0)
146 | security (= 0.1.3)
147 | simctl (~> 1.6.3)
148 | terminal-notifier (>= 2.0.0, < 3.0.0)
149 | terminal-table (>= 1.4.5, < 2.0.0)
150 | tty-screen (>= 0.6.3, < 1.0.0)
151 | tty-spinner (>= 0.8.0, < 1.0.0)
152 | word_wrap (~> 1.0.0)
153 | xcodeproj (>= 1.13.0, < 2.0.0)
154 | xcpretty (~> 0.3.0)
155 | xcpretty-travis-formatter (>= 0.0.3)
156 | ffi (1.15.5)
157 | fourflusher (2.3.1)
158 | fuzzy_match (2.0.4)
159 | gh_inspector (1.1.3)
160 | google-apis-androidpublisher_v3 (0.23.0)
161 | google-apis-core (>= 0.6, < 2.a)
162 | google-apis-core (0.6.0)
163 | addressable (~> 2.5, >= 2.5.1)
164 | googleauth (>= 0.16.2, < 2.a)
165 | httpclient (>= 2.8.1, < 3.a)
166 | mini_mime (~> 1.0)
167 | representable (~> 3.0)
168 | retriable (>= 2.0, < 4.a)
169 | rexml
170 | webrick
171 | google-apis-iamcredentials_v1 (0.12.0)
172 | google-apis-core (>= 0.6, < 2.a)
173 | google-apis-playcustomapp_v1 (0.9.0)
174 | google-apis-core (>= 0.6, < 2.a)
175 | google-apis-storage_v1 (0.16.0)
176 | google-apis-core (>= 0.6, < 2.a)
177 | google-cloud-core (1.6.0)
178 | google-cloud-env (~> 1.0)
179 | google-cloud-errors (~> 1.0)
180 | google-cloud-env (1.6.0)
181 | faraday (>= 0.17.3, < 3.0)
182 | google-cloud-errors (1.2.0)
183 | google-cloud-storage (1.36.2)
184 | addressable (~> 2.8)
185 | digest-crc (~> 0.4)
186 | google-apis-iamcredentials_v1 (~> 0.1)
187 | google-apis-storage_v1 (~> 0.1)
188 | google-cloud-core (~> 1.6)
189 | googleauth (>= 0.16.2, < 2.a)
190 | mini_mime (~> 1.0)
191 | googleauth (1.2.0)
192 | faraday (>= 0.17.3, < 3.a)
193 | jwt (>= 1.4, < 3.0)
194 | memoist (~> 0.16)
195 | multi_json (~> 1.11)
196 | os (>= 0.9, < 2.0)
197 | signet (>= 0.16, < 2.a)
198 | highline (2.0.3)
199 | http-cookie (1.0.5)
200 | domain_name (~> 0.5)
201 | httpclient (2.8.3)
202 | i18n (1.10.0)
203 | concurrent-ruby (~> 1.0)
204 | jmespath (1.6.1)
205 | json (2.6.2)
206 | jwt (2.4.1)
207 | memoist (0.16.2)
208 | mini_magick (4.11.0)
209 | mini_mime (1.1.2)
210 | minitest (5.16.1)
211 | molinillo (0.6.6)
212 | multi_json (1.15.0)
213 | multipart-post (2.0.0)
214 | nanaimo (0.3.0)
215 | nap (1.1.0)
216 | naturally (2.2.1)
217 | netrc (0.11.0)
218 | os (1.1.4)
219 | plist (3.6.0)
220 | public_suffix (4.0.7)
221 | rake (13.0.6)
222 | representable (3.2.0)
223 | declarative (< 0.1.0)
224 | trailblazer-option (>= 0.1.1, < 0.2.0)
225 | uber (< 0.2.0)
226 | retriable (3.1.2)
227 | rexml (3.2.5)
228 | rouge (2.0.7)
229 | ruby-macho (1.4.0)
230 | ruby2_keywords (0.0.5)
231 | rubyzip (2.3.2)
232 | security (0.1.3)
233 | signet (0.17.0)
234 | addressable (~> 2.8)
235 | faraday (>= 0.17.5, < 3.a)
236 | jwt (>= 1.5, < 3.0)
237 | multi_json (~> 1.10)
238 | simctl (1.6.8)
239 | CFPropertyList
240 | naturally
241 | terminal-notifier (2.0.0)
242 | terminal-table (1.8.0)
243 | unicode-display_width (~> 1.1, >= 1.1.1)
244 | thread_safe (0.3.6)
245 | trailblazer-option (0.1.2)
246 | tty-cursor (0.7.1)
247 | tty-screen (0.8.1)
248 | tty-spinner (0.9.3)
249 | tty-cursor (~> 0.7)
250 | typhoeus (1.4.0)
251 | ethon (>= 0.9.0)
252 | tzinfo (1.2.10)
253 | thread_safe (~> 0.1)
254 | uber (0.1.0)
255 | unf (0.1.4)
256 | unf_ext
257 | unf_ext (0.0.8.2)
258 | unicode-display_width (1.8.0)
259 | webrick (1.7.0)
260 | word_wrap (1.0.0)
261 | xcodeproj (1.22.0)
262 | CFPropertyList (>= 2.3.3, < 4.0)
263 | atomos (~> 0.1.3)
264 | claide (>= 1.0.2, < 2.0)
265 | colored2 (~> 3.1)
266 | nanaimo (~> 0.3.0)
267 | rexml (~> 3.2.4)
268 | xcpretty (0.3.0)
269 | rouge (~> 2.0.7)
270 | xcpretty-travis-formatter (1.0.1)
271 | xcpretty (~> 0.2, >= 0.0.7)
272 |
273 | PLATFORMS
274 | ruby
275 | x86_64-darwin-18
276 |
277 | DEPENDENCIES
278 | cocoapods (~> 1.10.2)
279 | fastlane (~> 2.191.0)
280 |
281 | RUBY VERSION
282 | ruby 3.0.2p107
283 |
284 | BUNDLED WITH
285 | 2.2.22
286 |
--------------------------------------------------------------------------------
/cli-tools/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakLoaderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweakLoaderTests.swift
3 | // Copyright © 2021 Just Eat Takeaway. All rights reserved.
4 | //
5 |
6 | import XCTest
7 |
8 | class TweakLoaderTests: XCTestCase {
9 |
10 | var sut: TweakLoader!
11 |
12 | override func setUp() {
13 | super.setUp()
14 | sut = TweakLoader()
15 | }
16 |
17 | override func tearDown() {
18 | sut = nil
19 | super.tearDown()
20 | }
21 |
22 | func test_loadConfiguration_success() throws {
23 | let bundle = Bundle(for: type(of: self))
24 | let tweaksFilename = "Tweaks"
25 | let tweaksFilePath = bundle.path(forResource: tweaksFilename, ofType: "json")!
26 |
27 | let testTweaks = try sut.load(tweaksFilePath)
28 |
29 | let expectedTweaks = [
30 | Tweak(feature: "general",
31 | variable: "answer_to_the_universe",
32 | title: "Definitive answer",
33 | description: "Answer to the Ultimate Question of Life, the Universe, and Everything",
34 | group: "General",
35 | valueType: "Int",
36 | propertyName: "definitiveAnswer"),
37 | Tweak(feature: "general",
38 | variable: "encrypted_answer_to_the_universe",
39 | title: "Encrypted definitive answer",
40 | description: "Encrypted answer to the Ultimate Question of Life, the Universe, and Everything",
41 | group: "General",
42 | valueType: "String",
43 | propertyName: "definitiveAnswerEncrypted"),
44 | Tweak(feature: "general",
45 | variable: "greet_on_app_did_become_active",
46 | title: "Greet on app launch",
47 | description: "shows an alert on applicationDidBecomeActive",
48 | group: "General",
49 | valueType: "Bool",
50 | propertyName: nil),
51 | Tweak(feature: "general",
52 | variable: "tap_to_change_color_enabled",
53 | title: "Tap to change views color",
54 | description: "change the colour of the main view when receiving a tap",
55 | group: "General",
56 | valueType: "Bool",
57 | propertyName: nil),
58 | Tweak(feature: "ui_customization",
59 | variable: "display_green_view",
60 | title: "Display Green View",
61 | description: "shows a green view in the main view controller",
62 | group: "UI Customization",
63 | valueType: "Bool",
64 | propertyName: nil),
65 | Tweak(feature: "ui_customization",
66 | variable: "display_red_view",
67 | title: "Display Red View",
68 | description: "shows a red view in the main view controller",
69 | group: "UI Customization",
70 | valueType: "Bool",
71 | propertyName: nil),
72 | Tweak(feature: "ui_customization",
73 | variable: "display_yellow_view",
74 | title: "Display Yellow View",
75 | description: "shows a yellow view in the main view controller",
76 | group: "UI Customization",
77 | valueType: "Bool",
78 | propertyName: nil),
79 | Tweak(feature: "ui_customization",
80 | variable: "label_text",
81 | title: "Label Text",
82 | description: "the title of the main label",
83 | group: "UI Customization",
84 | valueType: "String",
85 | propertyName: nil),
86 | Tweak(feature: "ui_customization",
87 | variable: "red_view_alpha_component",
88 | title: "Red View Alpha Component",
89 | description: "defines the alpha level of the red view",
90 | group: "UI Customization",
91 | valueType: "Double",
92 | propertyName: nil)]
93 | XCTAssertEqual(testTweaks, expectedTweaks)
94 | }
95 |
96 | func test_loadConfiguration_failure_invalidJSON() throws {
97 | let bundle = Bundle(for: type(of: self))
98 | let tweaksFilename = "InvalidTweaks_InvalidJSON"
99 | let tweaksFilePath = bundle.path(forResource: tweaksFilename, ofType: "json")!
100 |
101 | XCTAssertThrowsError(try sut.load(tweaksFilePath))
102 | }
103 |
104 | func test_loadConfiguration_failure_missingValues() throws {
105 | let bundle = Bundle(for: type(of: self))
106 | let tweaksFilename = "InvalidTweaks_MissingValues"
107 | let tweaksFilePath = bundle.path(forResource: tweaksFilename, ofType: "json")!
108 |
109 | XCTAssertThrowsError(try sut.load(tweaksFilePath))
110 | }
111 |
112 | func test_loadConfiguration_failure_duplicateGeneratedPropertyName() throws {
113 | let bundle = Bundle(for: type(of: self))
114 | let tweaksFilename = "InvalidTweaks_DuplicateGeneratedPropertyName"
115 | let tweaksFilePath = bundle.path(forResource: tweaksFilename, ofType: "json")!
116 | XCTAssertThrowsError(try sut.load(tweaksFilePath))
117 | }
118 |
119 | func test_typeForValue_String() {
120 | let expectedValue = "String"
121 | XCTAssertEqual(try sut.type(for: "some string"), expectedValue)
122 | }
123 |
124 | func test_typeForValue_NSNumber_Double() {
125 | let expectedValue = "Double"
126 | XCTAssertEqual(try sut.type(for: NSNumber(value: 3.14)), expectedValue)
127 | }
128 |
129 | func test_typeForValue_NSNumber_Int() {
130 | let expectedValue = "Int"
131 | XCTAssertEqual(try sut.type(for: NSNumber(value: 42)), expectedValue)
132 | }
133 |
134 | func test_typeForValue_NSNumber_Bool() {
135 | let expectedValue = "Bool"
136 | XCTAssertEqual(try sut.type(for: NSNumber(value: true)), expectedValue)
137 | }
138 |
139 | func test_typeForValue_Bool() {
140 | let expectedValue = "Bool"
141 | XCTAssertEqual(try sut.type(for: true), expectedValue)
142 | }
143 |
144 | func test_typeForValue_Double() {
145 | let expectedValue = "Double"
146 | XCTAssertEqual(try sut.type(for: 3.14), expectedValue)
147 | }
148 |
149 | func test_typeForValue_Unsupported() {
150 | XCTAssertThrowsError(try sut.type(for: [""]))
151 | }
152 |
153 | func test_tweakForDictionary_AllRequiredValuesPresent() throws {
154 | let feature = "some feature"
155 | let variable = "some variable"
156 | let title = "some title"
157 | let description = "some description"
158 | let group = "some group"
159 | let dictionary: [String : Any] = [
160 | "Title": title,
161 | "Description": description,
162 | "Group": group,
163 | "Value": 3.14
164 | ]
165 | let expectedValue = Tweak(feature: feature,
166 | variable: variable,
167 | title: title,
168 | description: description,
169 | group: group,
170 | valueType: "Double",
171 | propertyName: nil)
172 | let testValue = try sut.tweak(for: dictionary,
173 | feature: feature,
174 | variable: variable)
175 | XCTAssertEqual(testValue, expectedValue)
176 | }
177 |
178 | func test_tweakForDictionary_MissingTitle() {
179 | let feature = "some feature"
180 | let variable = "some variable"
181 | let description = "some description"
182 | let group = "some group"
183 | let dictionary: [String : Any] = [
184 | "Description": description,
185 | "Group": group,
186 | "Value": 3.14
187 | ]
188 | XCTAssertThrowsError(try sut.tweak(for: dictionary, feature: feature, variable: variable))
189 | }
190 |
191 | func test_tweakForDictionary_MissingDescription() {
192 | let feature = "some feature"
193 | let variable = "some variable"
194 | let title = "some title"
195 | let group = "some group"
196 | let dictionary: [String : Any] = [
197 | "Title": title,
198 | "Group": group,
199 | "Value": 3.14
200 | ]
201 | XCTAssertNoThrow(try sut.tweak(for: dictionary, feature: feature, variable: variable))
202 | }
203 |
204 | func test_tweakForDictionary_MissingGroup() {
205 | let feature = "some feature"
206 | let variable = "some variable"
207 | let title = "some title"
208 | let description = "some description"
209 | let dictionary: [String : Any] = [
210 | "Title": title,
211 | "Description": description,
212 | "Value": 3.14
213 | ]
214 | XCTAssertThrowsError(try sut.tweak(for: dictionary, feature: feature, variable: variable))
215 | }
216 |
217 | func test_tweakForDictionary_MissingValue() {
218 | let feature = "some feature"
219 | let variable = "some variable"
220 | let title = "some title"
221 | let description = "some description"
222 | let group = "some group"
223 | let dictionary: [String : Any] = [
224 | "Title": title,
225 | "Description": description,
226 | "Group": group
227 | ]
228 | XCTAssertThrowsError(try sut.tweak(for: dictionary, feature: feature, variable: variable))
229 | }
230 | }
231 |
--------------------------------------------------------------------------------