diff --git a/.gitignore b/.gitignore index 9a73f4b..38a14d1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ profile DerivedData *.hmap *.ipa +*.orig # Bundler .bundle @@ -35,3 +36,4 @@ Carthage/Build # `pod install` in .travis.yml # Pods/ +default.profraw diff --git a/Example/JustTweak.xcodeproj/project.pbxproj b/Example/JustTweak.xcodeproj/project.pbxproj index e6fd97f..c124c69 100644 --- a/Example/JustTweak.xcodeproj/project.pbxproj +++ b/Example/JustTweak.xcodeproj/project.pbxproj @@ -7,16 +7,19 @@ objects = { /* Begin PBXBuildFile section */ + 12A3231E2630365500468F8C /* GeneratedConfigurationAccessor+ExampleProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A3231A2630365500468F8C /* GeneratedConfigurationAccessor+ExampleProtocol.swift */; }; + 12A3231F2630365500468F8C /* GeneratedTweakAccessor+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A3231B2630365500468F8C /* GeneratedTweakAccessor+Constants.swift */; }; + 12A323202630365500468F8C /* TweakAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A3231C2630365500468F8C /* TweakAccessor.swift */; }; + 12A323212630365500468F8C /* GeneratedTweakAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A3231D2630365500468F8C /* GeneratedTweakAccessor.swift */; }; + 12EB283F262F321500A7A89A /* ExampleProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EB283E262F321500A7A89A /* ExampleProtocol.swift */; }; 4F0372B22387FE79003CA58E /* TweakManagerCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0372B12387FE79003CA58E /* TweakManagerCacheTests.swift */; }; - 4F30EA6E21ADA06A00AECF77 /* FirebaseConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F30EA6B21ADA06A00AECF77 /* FirebaseConfiguration.swift */; }; - 4F30EA6F21ADA06A00AECF77 /* OptimizelyConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F30EA6C21ADA06A00AECF77 /* OptimizelyConfiguration.swift */; }; - 4F38E0EF236DF6E20070BD17 /* ConfigurationAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F38E0EE236DF6E20070BD17 /* ConfigurationAccessor.swift */; }; 4F38E0F1236E1BB60070BD17 /* PropertyWrappersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F38E0F0236E1BB60070BD17 /* PropertyWrappersTests.swift */; }; 4F43583223E732DD00FD60A7 /* TweakManager+PresentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F43583123E732DD00FD60A7 /* TweakManager+PresentationTests.swift */; }; - 4F43583423E7332D00FD60A7 /* test_configuration_override.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F43583323E7332D00FD60A7 /* test_configuration_override.json */; }; + 4F43583423E7332D00FD60A7 /* LocalTweaks_test_override.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F43583323E7332D00FD60A7 /* LocalTweaks_test_override.json */; }; 4F6422A321A2FF8A002B69F0 /* ExampleOptimizelyDatafile.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F6422A221A2FF8A002B69F0 /* ExampleOptimizelyDatafile.json */; }; 4F6422A621A4205B002B69F0 /* Symbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6422A521A4205B002B69F0 /* Symbols.swift */; }; 4F6422A721A4205B002B69F0 /* Symbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6422A521A4205B002B69F0 /* Symbols.swift */; }; + 4F65E89F2629D083009E8C3B /* LocalTweaks_example.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F65E89E2629D083009E8C3B /* LocalTweaks_example.json */; }; 4F7103EE23859CFE0021E4AF /* CustomOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7103ED23859CFD0021E4AF /* CustomOperators.swift */; }; 51A8BD6A5E5948F41EEC2E66 /* Pods_JustTweak_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F594AA2DDEDFFA4C835EACCF /* Pods_JustTweak_Tests.framework */; }; 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; @@ -25,18 +28,16 @@ 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 618A46CC1DAD481400B8F5CA /* TweakExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618A46CB1DAD481400B8F5CA /* TweakExtensionsTests.swift */; }; - 618A46CE1DAD4DC500B8F5CA /* UserDefaultsTweaksConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618A46CD1DAD4DC500B8F5CA /* UserDefaultsTweaksConfigurationTests.swift */; }; + 618A46CE1DAD4DC500B8F5CA /* UserDefaultsTweakProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618A46CD1DAD4DC500B8F5CA /* UserDefaultsTweakProviderTests.swift */; }; 619938D81DA7F2DA00765852 /* TweakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619938D71DA7F2DA00765852 /* TweakTests.swift */; }; 619938DC1DA7F6E600765852 /* TweakViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619938DB1DA7F6E600765852 /* TweakViewControllerTests.swift */; }; - 619938DE1DA7F9FA00765852 /* test_configuration_no_displayable_ungrouped.json in Resources */ = {isa = PBXBuildFile; fileRef = 619938DD1DA7F9FA00765852 /* test_configuration_no_displayable_ungrouped.json */; }; - 619938E01DA805BA00765852 /* LocalConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619938DF1DA805BA00765852 /* LocalConfigurationTests.swift */; }; - 619938E21DA8061900765852 /* test_configuration_invalid.json in Resources */ = {isa = PBXBuildFile; fileRef = 619938E11DA8061900765852 /* test_configuration_invalid.json */; }; + 619938E01DA805BA00765852 /* LocalTweakProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619938DF1DA805BA00765852 /* LocalTweakProviderTests.swift */; }; 61A561391DA6AC4B00839F7F /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 61A561381DA6AC4B00839F7F /* GoogleService-Info.plist */; }; - 61A561411DA7B65200839F7F /* test_configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = 61A5613F1DA7B64900839F7F /* test_configuration.json */; }; + 61A561411DA7B65200839F7F /* LocalTweaks_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 61A5613F1DA7B64900839F7F /* LocalTweaks_test.json */; }; 61BE051B1DAE868900288EC7 /* BooleanTweakTableViewCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BE051A1DAE868900288EC7 /* BooleanTweakTableViewCellTests.swift */; }; 61BE051E1DAE878600288EC7 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BE051D1DAE878600288EC7 /* TestHelpers.swift */; }; 61BE05201DAE88A100288EC7 /* TextTweakTableViewCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BE051F1DAE88A100288EC7 /* TextTweakTableViewCellTests.swift */; }; - 61D2DEBB1D9ECD4D00E6C6FA /* ExampleConfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 61D2DEBA1D9ECD4D00E6C6FA /* ExampleConfiguration.json */; }; + 61D2DEBB1D9ECD4D00E6C6FA /* LocalTweaks_TopPriority_example.json in Resources */ = {isa = PBXBuildFile; fileRef = 61D2DEBA1D9ECD4D00E6C6FA /* LocalTweaks_TopPriority_example.json */; }; 61F1DECE1DA7ECF700314DEC /* TweaksUtilitiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1DECD1DA7ECF700314DEC /* TweaksUtilitiesTests.swift */; }; 61F1DECF1DA7EE6C00314DEC /* TweakManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1DEC91DA7ECD300314DEC /* TweakManagerTests.swift */; }; 8D0BA4A3FCD875282B8A3ED3 /* Pods_JustTweak_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B347460C9476DF9F5F7BDF32 /* Pods_JustTweak_Example.framework */; }; @@ -53,18 +54,24 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 12A3231A2630365500468F8C /* GeneratedConfigurationAccessor+ExampleProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GeneratedConfigurationAccessor+ExampleProtocol.swift"; sourceTree = ""; }; + 12A3231B2630365500468F8C /* GeneratedTweakAccessor+Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GeneratedTweakAccessor+Constants.swift"; sourceTree = ""; }; + 12A3231C2630365500468F8C /* TweakAccessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweakAccessor.swift; sourceTree = ""; }; + 12A3231D2630365500468F8C /* GeneratedTweakAccessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedTweakAccessor.swift; sourceTree = ""; }; + 12EB283E262F321500A7A89A /* ExampleProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleProtocol.swift; sourceTree = ""; }; 2A3A7CF0048CBC9CF1917718 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 3432AE8768ED164F737437EA /* Pods-JustTweak_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JustTweak_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-JustTweak_Tests/Pods-JustTweak_Tests.release.xcconfig"; sourceTree = ""; }; 4F0372B12387FE79003CA58E /* TweakManagerCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweakManagerCacheTests.swift; sourceTree = ""; }; - 4F30EA6B21ADA06A00AECF77 /* FirebaseConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirebaseConfiguration.swift; sourceTree = ""; }; - 4F30EA6C21ADA06A00AECF77 /* OptimizelyConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyConfiguration.swift; sourceTree = ""; }; - 4F38E0EE236DF6E20070BD17 /* ConfigurationAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationAccessor.swift; sourceTree = ""; }; + 4F30EA6B21ADA06A00AECF77 /* FirebaseTweakProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirebaseTweakProvider.swift; sourceTree = ""; }; + 4F30EA6C21ADA06A00AECF77 /* OptimizelyTweakProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyTweakProvider.swift; sourceTree = ""; }; 4F38E0F0236E1BB60070BD17 /* PropertyWrappersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyWrappersTests.swift; sourceTree = ""; }; 4F43583123E732DD00FD60A7 /* TweakManager+PresentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TweakManager+PresentationTests.swift"; sourceTree = ""; }; - 4F43583323E7332D00FD60A7 /* test_configuration_override.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test_configuration_override.json; sourceTree = ""; }; + 4F43583323E7332D00FD60A7 /* LocalTweaks_test_override.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = LocalTweaks_test_override.json; sourceTree = ""; }; 4F6422A221A2FF8A002B69F0 /* ExampleOptimizelyDatafile.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ExampleOptimizelyDatafile.json; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.javascript; }; 4F6422A521A4205B002B69F0 /* Symbols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Symbols.swift; sourceTree = ""; }; + 4F65E89E2629D083009E8C3B /* LocalTweaks_example.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = LocalTweaks_example.json; sourceTree = ""; }; 4F7103ED23859CFD0021E4AF /* CustomOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOperators.swift; sourceTree = ""; }; + 4FFF3DC5262B1789002A674D /* config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = config.json; sourceTree = ""; }; 607FACD01AFB9204008FA782 /* JustTweak_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JustTweak_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -75,18 +82,16 @@ 607FACE51AFB9204008FA782 /* JustTweak_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JustTweak_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 618A46CB1DAD481400B8F5CA /* TweakExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweakExtensionsTests.swift; sourceTree = ""; }; - 618A46CD1DAD4DC500B8F5CA /* UserDefaultsTweaksConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsTweaksConfigurationTests.swift; sourceTree = ""; }; + 618A46CD1DAD4DC500B8F5CA /* UserDefaultsTweakProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsTweakProviderTests.swift; sourceTree = ""; }; 619938D71DA7F2DA00765852 /* TweakTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweakTests.swift; sourceTree = ""; }; 619938DB1DA7F6E600765852 /* TweakViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweakViewControllerTests.swift; sourceTree = ""; }; - 619938DD1DA7F9FA00765852 /* test_configuration_no_displayable_ungrouped.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test_configuration_no_displayable_ungrouped.json; sourceTree = ""; }; - 619938DF1DA805BA00765852 /* LocalConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalConfigurationTests.swift; sourceTree = ""; }; - 619938E11DA8061900765852 /* test_configuration_invalid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test_configuration_invalid.json; sourceTree = ""; }; + 619938DF1DA805BA00765852 /* LocalTweakProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalTweakProviderTests.swift; sourceTree = ""; }; 61A561381DA6AC4B00839F7F /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 61A5613F1DA7B64900839F7F /* test_configuration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = test_configuration.json; sourceTree = ""; }; + 61A5613F1DA7B64900839F7F /* LocalTweaks_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = LocalTweaks_test.json; sourceTree = ""; }; 61BE051A1DAE868900288EC7 /* BooleanTweakTableViewCellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BooleanTweakTableViewCellTests.swift; sourceTree = ""; }; 61BE051D1DAE878600288EC7 /* TestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; }; 61BE051F1DAE88A100288EC7 /* TextTweakTableViewCellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextTweakTableViewCellTests.swift; sourceTree = ""; }; - 61D2DEBA1D9ECD4D00E6C6FA /* ExampleConfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ExampleConfiguration.json; sourceTree = ""; }; + 61D2DEBA1D9ECD4D00E6C6FA /* LocalTweaks_TopPriority_example.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = LocalTweaks_TopPriority_example.json; sourceTree = ""; }; 61F1DEC91DA7ECD300314DEC /* TweakManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweakManagerTests.swift; sourceTree = ""; }; 61F1DECD1DA7ECF700314DEC /* TweaksUtilitiesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweaksUtilitiesTests.swift; sourceTree = ""; }; 6817DC526CBFCF55440CC371 /* Pods-JustTweak_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JustTweak_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-JustTweak_Tests/Pods-JustTweak_Tests.debug.xcconfig"; sourceTree = ""; }; @@ -118,13 +123,34 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 4F30EA6A21ADA06A00AECF77 /* Configurations */ = { + 12A323192630365500468F8C /* Accessors */ = { isa = PBXGroup; children = ( - 4F30EA6B21ADA06A00AECF77 /* FirebaseConfiguration.swift */, - 4F30EA6C21ADA06A00AECF77 /* OptimizelyConfiguration.swift */, + 12A3231A2630365500468F8C /* GeneratedConfigurationAccessor+ExampleProtocol.swift */, + 12A3231D2630365500468F8C /* GeneratedTweakAccessor.swift */, + 12A3231B2630365500468F8C /* GeneratedTweakAccessor+Constants.swift */, + 12A3231C2630365500468F8C /* TweakAccessor.swift */, ); - path = Configurations; + path = Accessors; + sourceTree = ""; + }; + 12EB2842262F322000A7A89A /* Protocols */ = { + isa = PBXGroup; + children = ( + 12EB283E262F321500A7A89A /* ExampleProtocol.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + 4F30EA6A21ADA06A00AECF77 /* TweakProviders */ = { + isa = PBXGroup; + children = ( + 4F30EA6B21ADA06A00AECF77 /* FirebaseTweakProvider.swift */, + 4F30EA6C21ADA06A00AECF77 /* OptimizelyTweakProvider.swift */, + 4F65E89E2629D083009E8C3B /* LocalTweaks_example.json */, + 61D2DEBA1D9ECD4D00E6C6FA /* LocalTweaks_TopPriority_example.json */, + ); + path = TweakProviders; sourceTree = ""; }; 4F6422A421A4205B002B69F0 /* Shared */ = { @@ -135,6 +161,42 @@ path = Shared; sourceTree = ""; }; + 4F65E6172629B86F009E8C3B /* UI */ = { + isa = PBXGroup; + children = ( + 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, + 607FACD91AFB9204008FA782 /* Main.storyboard */, + ); + path = UI; + sourceTree = ""; + }; + 4F65E6182629B880009E8C3B /* Assets */ = { + isa = PBXGroup; + children = ( + 61A561381DA6AC4B00839F7F /* GoogleService-Info.plist */, + 4F6422A221A2FF8A002B69F0 /* ExampleOptimizelyDatafile.json */, + 607FACDC1AFB9204008FA782 /* Images.xcassets */, + ); + path = Assets; + sourceTree = ""; + }; + 4F65E61B2629B8A2009E8C3B /* Code */ = { + isa = PBXGroup; + children = ( + 607FACD51AFB9204008FA782 /* AppDelegate.swift */, + 607FACD71AFB9204008FA782 /* ViewController.swift */, + ); + path = Code; + sourceTree = ""; + }; + 4FFF3DC4262B1789002A674D /* CodeGeneration */ = { + isa = PBXGroup; + children = ( + 4FFF3DC5262B1789002A674D /* config.json */, + ); + path = CodeGeneration; + sourceTree = ""; + }; 607FACC71AFB9204008FA782 = { isa = PBXGroup; children = ( @@ -160,17 +222,14 @@ 607FACD21AFB9204008FA782 /* Example for JustTweak */ = { isa = PBXGroup; children = ( - 4F30EA6A21ADA06A00AECF77 /* Configurations */, - 61D2DEBA1D9ECD4D00E6C6FA /* ExampleConfiguration.json */, - 4F6422A221A2FF8A002B69F0 /* ExampleOptimizelyDatafile.json */, - 61A561381DA6AC4B00839F7F /* GoogleService-Info.plist */, - 607FACD51AFB9204008FA782 /* AppDelegate.swift */, - 607FACD71AFB9204008FA782 /* ViewController.swift */, - 4F38E0EE236DF6E20070BD17 /* ConfigurationAccessor.swift */, - 607FACDC1AFB9204008FA782 /* Images.xcassets */, - 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, - 607FACD91AFB9204008FA782 /* Main.storyboard */, + 12A323192630365500468F8C /* Accessors */, + 4F65E6182629B880009E8C3B /* Assets */, + 4F65E61B2629B8A2009E8C3B /* Code */, + 4FFF3DC4262B1789002A674D /* CodeGeneration */, + 4F30EA6A21ADA06A00AECF77 /* TweakProviders */, + 12EB2842262F322000A7A89A /* Protocols */, 607FACD31AFB9204008FA782 /* Supporting Files */, + 4F65E6172629B86F009E8C3B /* UI */, ); name = "Example for JustTweak"; path = JustTweak; @@ -198,10 +257,8 @@ 607FACE91AFB9204008FA782 /* Supporting Files */ = { isa = PBXGroup; children = ( - 619938E11DA8061900765852 /* test_configuration_invalid.json */, - 619938DD1DA7F9FA00765852 /* test_configuration_no_displayable_ungrouped.json */, - 61A5613F1DA7B64900839F7F /* test_configuration.json */, - 4F43583323E7332D00FD60A7 /* test_configuration_override.json */, + 61A5613F1DA7B64900839F7F /* LocalTweaks_test.json */, + 4F43583323E7332D00FD60A7 /* LocalTweaks_test_override.json */, 607FACEA1AFB9204008FA782 /* Info.plist */, ); name = "Supporting Files"; @@ -229,14 +286,14 @@ 61F1DEC81DA7ECD300314DEC /* Core */ = { isa = PBXGroup; children = ( - 619938DF1DA805BA00765852 /* LocalConfigurationTests.swift */, + 619938DF1DA805BA00765852 /* LocalTweakProviderTests.swift */, 618A46CB1DAD481400B8F5CA /* TweakExtensionsTests.swift */, 61F1DEC91DA7ECD300314DEC /* TweakManagerTests.swift */, 4F43583123E732DD00FD60A7 /* TweakManager+PresentationTests.swift */, 4F0372B12387FE79003CA58E /* TweakManagerCacheTests.swift */, 61F1DECD1DA7ECF700314DEC /* TweaksUtilitiesTests.swift */, 619938D71DA7F2DA00765852 /* TweakTests.swift */, - 618A46CD1DAD4DC500B8F5CA /* UserDefaultsTweaksConfigurationTests.swift */, + 618A46CD1DAD4DC500B8F5CA /* UserDefaultsTweakProviderTests.swift */, 4F38E0F0236E1BB60070BD17 /* PropertyWrappersTests.swift */, ); path = Core; @@ -280,6 +337,7 @@ buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "JustTweak_Example" */; buildPhases = ( D768FEA0BAC54557A2874E32 /* [CP] Check Pods Manifest.lock */, + AE2D758BBB214ECA1116D055 /* [CP-User] TweakAccessorGenerator */, 607FACCC1AFB9204008FA782 /* Sources */, 607FACCD1AFB9204008FA782 /* Frameworks */, 607FACCE1AFB9204008FA782 /* Resources */, @@ -360,8 +418,9 @@ buildActionMask = 2147483647; files = ( 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, - 61D2DEBB1D9ECD4D00E6C6FA /* ExampleConfiguration.json in Resources */, + 61D2DEBB1D9ECD4D00E6C6FA /* LocalTweaks_TopPriority_example.json in Resources */, 61A561391DA6AC4B00839F7F /* GoogleService-Info.plist in Resources */, + 4F65E89F2629D083009E8C3B /* LocalTweaks_example.json in Resources */, 4F6422A321A2FF8A002B69F0 /* ExampleOptimizelyDatafile.json in Resources */, 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, @@ -372,16 +431,24 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4F43583423E7332D00FD60A7 /* test_configuration_override.json in Resources */, - 619938E21DA8061900765852 /* test_configuration_invalid.json in Resources */, - 619938DE1DA7F9FA00765852 /* test_configuration_no_displayable_ungrouped.json in Resources */, - 61A561411DA7B65200839F7F /* test_configuration.json in Resources */, + 4F43583423E7332D00FD60A7 /* LocalTweaks_test_override.json in Resources */, + 61A561411DA7B65200839F7F /* LocalTweaks_test.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + AE2D758BBB214ECA1116D055 /* [CP-User] TweakAccessorGenerator */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + name = "[CP-User] TweakAccessorGenerator"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "$SRCROOT/../JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator \\\n -l $SRCROOT/JustTweak/TweakProviders/LocalTweaks_example.json \\\n -o $SRCROOT/JustTweak/Accessors/ \\\n -c $SRCROOT/JustTweak/CodeGeneration/\n"; + }; D2F894A2A9B802CCF5D931F9 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -425,29 +492,11 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-JustTweak_Example/Pods-JustTweak_Example-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/JustTweak/JustTweak.framework", - "${BUILT_PRODUCTS_DIR}/OptimizelySDKCore/OptimizelySDKCore.framework", - "${BUILT_PRODUCTS_DIR}/OptimizelySDKDatafileManager/OptimizelySDKDatafileManager.framework", - "${BUILT_PRODUCTS_DIR}/OptimizelySDKEventDispatcher/OptimizelySDKEventDispatcher.framework", - "${BUILT_PRODUCTS_DIR}/OptimizelySDKShared/OptimizelySDKShared.framework", - "${BUILT_PRODUCTS_DIR}/OptimizelySDKUserProfileService/OptimizelySDKUserProfileService.framework", - "${BUILT_PRODUCTS_DIR}/OptimizelySDKiOS/OptimizelySDKiOS.framework", - "${BUILT_PRODUCTS_DIR}/Protobuf/protobuf.framework", - "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JustTweak.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OptimizelySDKCore.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OptimizelySDKDatafileManager.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OptimizelySDKEventDispatcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OptimizelySDKShared.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OptimizelySDKUserProfileService.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OptimizelySDKiOS.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/protobuf.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -461,12 +510,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4F30EA6E21ADA06A00AECF77 /* FirebaseConfiguration.swift in Sources */, + 12EB283F262F321500A7A89A /* ExampleProtocol.swift in Sources */, 4F6422A621A4205B002B69F0 /* Symbols.swift in Sources */, + 12A323202630365500468F8C /* TweakAccessor.swift in Sources */, + 12A3231E2630365500468F8C /* GeneratedConfigurationAccessor+ExampleProtocol.swift in Sources */, 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, - 4F30EA6F21ADA06A00AECF77 /* OptimizelyConfiguration.swift in Sources */, - 4F38E0EF236DF6E20070BD17 /* ConfigurationAccessor.swift in Sources */, + 12A323212630365500468F8C /* GeneratedTweakAccessor.swift in Sources */, 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, + 12A3231F2630365500468F8C /* GeneratedTweakAccessor+Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -479,11 +530,11 @@ 61BE051B1DAE868900288EC7 /* BooleanTweakTableViewCellTests.swift in Sources */, 61F1DECE1DA7ECF700314DEC /* TweaksUtilitiesTests.swift in Sources */, 61F1DECF1DA7EE6C00314DEC /* TweakManagerTests.swift in Sources */, - 619938E01DA805BA00765852 /* LocalConfigurationTests.swift in Sources */, + 619938E01DA805BA00765852 /* LocalTweakProviderTests.swift in Sources */, 4F7103EE23859CFE0021E4AF /* CustomOperators.swift in Sources */, 61BE05201DAE88A100288EC7 /* TextTweakTableViewCellTests.swift in Sources */, 4F38E0F1236E1BB60070BD17 /* PropertyWrappersTests.swift in Sources */, - 618A46CE1DAD4DC500B8F5CA /* UserDefaultsTweaksConfigurationTests.swift in Sources */, + 618A46CE1DAD4DC500B8F5CA /* UserDefaultsTweakProviderTests.swift in Sources */, 618A46CC1DAD481400B8F5CA /* TweakExtensionsTests.swift in Sources */, 4F6422A721A4205B002B69F0 /* Symbols.swift in Sources */, 619938DC1DA7F6E600765852 /* TweakViewControllerTests.swift in Sources */, @@ -573,6 +624,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = "-DCONFIGURATION_DEBUG"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; @@ -623,6 +675,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = "-DCONFIGURATION_DEBUG"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; diff --git a/Example/JustTweak.xcodeproj/xcshareddata/xcschemes/JustTweak-Example.xcscheme b/Example/JustTweak.xcodeproj/xcshareddata/xcschemes/JustTweak-Example.xcscheme index 941a150..60e7145 100644 --- a/Example/JustTweak.xcodeproj/xcshareddata/xcschemes/JustTweak-Example.xcscheme +++ b/Example/JustTweak.xcodeproj/xcshareddata/xcschemes/JustTweak-Example.xcscheme @@ -40,8 +40,17 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - codeCoverageEnabled = "YES" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + + + + @@ -54,17 +63,6 @@ - - - - - - - - + + diff --git a/Example/JustTweak.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/JustTweak.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..33a6cce --- /dev/null +++ b/Example/JustTweak.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "swift-argument-parser", + "repositoryURL": "git@github.com:apple/swift-argument-parser.git", + "state": { + "branch": null, + "revision": "831ed5e860a70e745bc1337830af4786b2576881", + "version": "0.4.1" + } + } + ] + }, + "version": 1 +} diff --git a/Example/JustTweak/Accessors/GeneratedConfigurationAccessor+ExampleProtocol.swift b/Example/JustTweak/Accessors/GeneratedConfigurationAccessor+ExampleProtocol.swift new file mode 100644 index 0000000..9c9fbd3 --- /dev/null +++ b/Example/JustTweak/Accessors/GeneratedConfigurationAccessor+ExampleProtocol.swift @@ -0,0 +1,8 @@ +// +// GeneratedConfigurationAccessor+ExampleProtocol.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +extension GeneratedTweakAccessor: ExampleProtocol { } diff --git a/Example/JustTweak/Accessors/GeneratedTweakAccessor+Constants.swift b/Example/JustTweak/Accessors/GeneratedTweakAccessor+Constants.swift new file mode 100644 index 0000000..28cd3a4 --- /dev/null +++ b/Example/JustTweak/Accessors/GeneratedTweakAccessor+Constants.swift @@ -0,0 +1,25 @@ +// +// GeneratedTweakAccessor+Constants.swift +// Generated by TweakAccessorGenerator +// + +import Foundation + +extension GeneratedTweakAccessor { + + struct Features { + static let general = "general" + static let uiCustomization = "ui_customization" + } + + struct Variables { + static let answerToTheUniverse = "answer_to_the_universe" + static let displayGreenView = "display_green_view" + static let displayRedView = "display_red_view" + static let displayYellowView = "display_yellow_view" + static let greetOnAppDidBecomeActive = "greet_on_app_did_become_active" + static let labelText = "label_text" + static let redViewAlphaComponent = "red_view_alpha_component" + static let tapToChangeColorEnabled = "tap_to_change_color_enabled" + } +} \ No newline at end of file diff --git a/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift b/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift new file mode 100644 index 0000000..244c7ea --- /dev/null +++ b/Example/JustTweak/Accessors/GeneratedTweakAccessor.swift @@ -0,0 +1,86 @@ +// +// GeneratedTweakAccessor.swift +// Generated by TweakAccessorGenerator +// + +import Foundation +import JustTweak + +class GeneratedTweakAccessor { + + static let tweakManager: TweakManager = { + var tweakProviders: [TweakProvider] = [] + + // EphemeralTweakProvider + #if DEBUG || CONFIGURATION_UI_TESTS + let ephemeralTweakProvider_1 = NSMutableDictionary() + tweakProviders.append(ephemeralTweakProvider_1) + #endif + + // UserDefaultsTweakProvider + #if DEBUG || CONFIGURATION_DEBUG + let userDefaultsTweakProvider_1 = UserDefaultsTweakProvider(userDefaults: UserDefaults.standard) + tweakProviders.append(userDefaultsTweakProvider_1) + #endif + + // LocalTweakProvider + #if DEBUG + let jsonFileURL_1 = Bundle.main.url(forResource: "LocalTweaks_TopPriority_example", withExtension: "json")! + let localTweakProvider_1 = LocalTweakProvider(jsonURL: jsonFileURL_1) + tweakProviders.append(localTweakProvider_1) + #endif + + // LocalTweakProvider + let jsonFileURL_2 = Bundle.main.url(forResource: "LocalTweaks_example", withExtension: "json")! + let localTweakProvider_2 = LocalTweakProvider(jsonURL: jsonFileURL_2) + tweakProviders.append(localTweakProvider_2) + + let tweakManager = TweakManager(tweakProviders: tweakProviders) + tweakManager.useCache = true + return tweakManager + }() + + var tweakManager: TweakManager { + return Self.tweakManager + } + + @TweakProperty(feature: Features.general, + variable: Variables.answerToTheUniverse, + tweakManager: tweakManager) + var meaningOfLife: Int + + @TweakProperty(feature: Features.general, + variable: Variables.greetOnAppDidBecomeActive, + tweakManager: tweakManager) + var shouldShowAlert: Bool + + @TweakProperty(feature: Features.general, + variable: Variables.tapToChangeColorEnabled, + tweakManager: tweakManager) + var isTapGestureToChangeColorEnabled: Bool + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.displayGreenView, + tweakManager: tweakManager) + var canShowGreenView: Bool + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.displayRedView, + tweakManager: tweakManager) + var canShowRedView: Bool + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.displayYellowView, + tweakManager: tweakManager) + var canShowYellowView: Bool + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.labelText, + tweakManager: tweakManager) + var labelText: String + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.redViewAlphaComponent, + tweakManager: tweakManager) + var redViewAlpha: Double +} \ No newline at end of file diff --git a/Example/JustTweak/Accessors/TweakAccessor.swift b/Example/JustTweak/Accessors/TweakAccessor.swift new file mode 100644 index 0000000..918ea91 --- /dev/null +++ b/Example/JustTweak/Accessors/TweakAccessor.swift @@ -0,0 +1,104 @@ +// +// TweakAccessor.swift +// Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved. +// + +import Foundation +import JustTweak + +class TweakAccessor { + + static let tweakManager: TweakManager = { + var tweakProviders: [TweakProvider] = [] + + // UserDefaultsTweakProvider + #if DEBUG || CONFIGURATION_DEBUG + let userDefaultsTweakProvider_1 = UserDefaultsTweakProvider(userDefaults: UserDefaults.standard) + tweakProviders.append(userDefaultsTweakProvider_1) + #endif + + // OptimizelyTweakProvider + // let optimizelyTweakProvider = OptimizelyTweakProvider() + // optimizelyTweakProvider.userId = UUID().uuidString + // tweakProviders.append(optimizelyTweakProvider) + + // FirebaseTweakProvider + // let firebaseTweakProvider = FirebaseTweakProvider() + // tweakProviders.append(firebaseTweakProvider) + + // LocalTweakProvider + #if CONFIGURATION_DEBUG + let jsonFileURL_1 = Bundle.main.url(forResource: "LocalTweaks_TopPriority_example", withExtension: "json")! + let localTweakProvider_1 = LocalTweakProvider(jsonURL: jsonFileURL_1) + tweakProviders.append(localTweakProvider_1) + #endif + + // LocalTweakProvider + let jsonFileURL_2 = Bundle.main.url(forResource: "LocalTweaks_example", withExtension: "json")! + let localTweakProvider_2 = LocalTweakProvider(jsonURL: jsonFileURL_2) + tweakProviders.append(localTweakProvider_2) + + return TweakManager(tweakProviders: tweakProviders) + }() + + private var tweakManager: TweakManager { + return Self.tweakManager + } + + // MARK: - Via Property Wrappers + + @FallbackTweakProperty(fallbackValue: false, + feature: Features.general, + variable: Variables.greetOnAppDidBecomeActive, + tweakManager: tweakManager) + var shouldShowAlert: Bool + + @FallbackTweakProperty(fallbackValue: false, + feature: Features.uiCustomization, + variable: Variables.displayRedView, + tweakManager: tweakManager) + var canShowRedView: Bool + + @FallbackTweakProperty(fallbackValue: false, + feature: Features.uiCustomization, + variable: Variables.displayGreenView, + tweakManager: tweakManager) + var canShowGreenView: Bool + + @FallbackTweakProperty(fallbackValue: "", + feature: Features.uiCustomization, + variable: Variables.labelText, + tweakManager: tweakManager) + var labelText: String + + @FallbackTweakProperty(fallbackValue: 42, + feature: Features.uiCustomization, + variable: Variables.meaningOfLife, + tweakManager: tweakManager) + var meaningOfLife: Int + + @OptionalTweakProperty(fallbackValue: nil, + feature: Features.uiCustomization, + variable: Variables.answerToTheUniverse, + tweakManager: tweakManager) + var optionalMeaningOfLife: Int? + + + // MARK: - Via TweakManager + + var canShowYellowView: Bool { + return tweakManager.tweakWith(feature: Features.uiCustomization, + variable: Variables.displayYellowView)?.boolValue ?? false + } + + var redViewAlpha: Float { + return tweakManager.tweakWith(feature: Features.uiCustomization, + variable: Variables.redViewAlpha)?.floatValue ?? 0.0 + } + + var isTapGestureToChangeColorEnabled: Bool { + return tweakManager.tweakWith(feature: Features.general, + variable: Variables.tapToChangeViewColor)?.boolValue ?? false + } +} + diff --git a/Example/JustTweak/ExampleOptimizelyDatafile.json b/Example/JustTweak/Assets/ExampleOptimizelyDatafile.json similarity index 100% rename from Example/JustTweak/ExampleOptimizelyDatafile.json rename to Example/JustTweak/Assets/ExampleOptimizelyDatafile.json diff --git a/Example/JustTweak/GoogleService-Info.plist b/Example/JustTweak/Assets/GoogleService-Info.plist similarity index 100% rename from Example/JustTweak/GoogleService-Info.plist rename to Example/JustTweak/Assets/GoogleService-Info.plist diff --git a/Example/JustTweak/Images.xcassets/AppIcon.appiconset/Contents.json b/Example/JustTweak/Assets/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Example/JustTweak/Images.xcassets/AppIcon.appiconset/Contents.json rename to Example/JustTweak/Assets/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/Example/JustTweak/AppDelegate.swift b/Example/JustTweak/Code/AppDelegate.swift similarity index 81% rename from Example/JustTweak/AppDelegate.swift rename to Example/JustTweak/Code/AppDelegate.swift index 5b24560..154f139 100644 --- a/Example/JustTweak/AppDelegate.swift +++ b/Example/JustTweak/Code/AppDelegate.swift @@ -10,18 +10,18 @@ import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - let configurationAccessor = ConfigurationAccessor() + let tweakAccessor = GeneratedTweakAccessor() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { let navigationController = window?.rootViewController as! UINavigationController let viewController = navigationController.topViewController as! ViewController - viewController.configurationAccessor = configurationAccessor - viewController.tweakManager = ConfigurationAccessor.tweakManager + viewController.tweakAccessor = tweakAccessor + viewController.tweakManager = TweakAccessor.tweakManager return true } func applicationDidBecomeActive(_ application: UIApplication) { - if configurationAccessor.shouldShowAlert { + if tweakAccessor.shouldShowAlert { let alertController = UIAlertController(title: "Hello", message: "Welcome to this Demo app!", preferredStyle: .alert) diff --git a/Example/JustTweak/ViewController.swift b/Example/JustTweak/Code/ViewController.swift similarity index 82% rename from Example/JustTweak/ViewController.swift rename to Example/JustTweak/Code/ViewController.swift index 9de6720..30399e9 100644 --- a/Example/JustTweak/ViewController.swift +++ b/Example/JustTweak/Code/ViewController.swift @@ -13,7 +13,7 @@ class ViewController: UIViewController { @IBOutlet var yellowView: UIView! @IBOutlet var mainLabel: UILabel! - var configurationAccessor: ConfigurationAccessor! + var tweakAccessor: GeneratedTweakAccessor! var tweakManager: TweakManager! private var tapGestureRecognizer: UITapGestureRecognizer! @@ -36,11 +36,11 @@ class ViewController: UIViewController { internal func updateView() { setUpGestures() - redView.isHidden = !configurationAccessor.canShowRedView - greenView.isHidden = !configurationAccessor.canShowGreenView - yellowView.isHidden = !configurationAccessor.canShowYellowView - mainLabel.text = configurationAccessor.labelText - redView.alpha = CGFloat(configurationAccessor.redViewAlpha) + redView.isHidden = !tweakAccessor.canShowRedView + greenView.isHidden = !tweakAccessor.canShowGreenView + yellowView.isHidden = !tweakAccessor.canShowYellowView + mainLabel.text = tweakAccessor.labelText + redView.alpha = CGFloat(tweakAccessor.redViewAlpha) } internal func setUpGestures() { @@ -48,13 +48,13 @@ class ViewController: UIViewController { tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(changeViewColor)) view.addGestureRecognizer(tapGestureRecognizer) } - tapGestureRecognizer.isEnabled = configurationAccessor.isTapGestureToChangeColorEnabled + tapGestureRecognizer.isEnabled = tweakAccessor.isTapGestureToChangeColorEnabled } @objc internal func setAndShowMeaningOfLife() { - configurationAccessor.meaningOfLife = Bool.random() ? 42 : nil + tweakAccessor.meaningOfLife = Bool.random() ? 42 : 108 let alertController = UIAlertController(title: "The Meaning of Life", - message: String(describing: configurationAccessor.meaningOfLife), + message: String(describing: tweakAccessor.meaningOfLife), preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) present(alertController, animated: true, completion: nil) diff --git a/Example/JustTweak/CodeGeneration/config.json b/Example/JustTweak/CodeGeneration/config.json new file mode 100644 index 0000000..17953e9 --- /dev/null +++ b/Example/JustTweak/CodeGeneration/config.json @@ -0,0 +1,24 @@ +{ + "tweakProviders": [ + { + "type": "EphemeralTweakProvider", + "macros": ["DEBUG", "CONFIGURATION_UI_TESTS"] + }, + { + "type": "UserDefaultsTweakProvider", + "parameter": "UserDefaults.standard", + "macros": ["DEBUG", "CONFIGURATION_DEBUG"] + }, + { + "type": "LocalTweakProvider", + "parameter": "LocalTweaks_TopPriority_example", + "macros": ["DEBUG"] + }, + { + "type": "LocalTweakProvider", + "parameter": "LocalTweaks_example" + } + ], + "shouldCacheTweaks": true, + "accessorName": "GeneratedTweakAccessor" +} diff --git a/Example/JustTweak/ConfigurationAccessor.swift b/Example/JustTweak/ConfigurationAccessor.swift deleted file mode 100644 index 5b4dd3c..0000000 --- a/Example/JustTweak/ConfigurationAccessor.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// ConfigurationAccessor.swift -// Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved. -// - -import Foundation -import JustTweak - -class ConfigurationAccessor { - - static let tweakManager: TweakManager = { - let userDefaultsConfiguration = UserDefaultsConfiguration(userDefaults: UserDefaults.standard) - - // let optimizelyConfiguration = OptimizelyTweaksConfiguration() - // optimizelyConfiguration.userId = UUID().uuidString - - // let firebaseConfiguration = FirebaseTweaksConfiguration() - - let jsonFileURL = Bundle.main.url(forResource: "ExampleConfiguration", withExtension: "json")! - let localConfiguration = LocalConfiguration(jsonURL: jsonFileURL) - - let configurations: [Configuration] = [userDefaultsConfiguration, localConfiguration] - // let configurations: [Configuration] = [userDefaultsConfiguration, optimizelyConfiguration, firebaseConfiguration, localConfiguration] - return TweakManager(configurations: configurations) - }() - - private var tweakManager: TweakManager { - return Self.tweakManager - } - - // MARK: - Via Property Wrappers - - @TweakProperty(fallbackValue: false, - feature: Features.General, - variable: Variables.GreetOnAppDidBecomeActive, - tweakManager: tweakManager) - var shouldShowAlert: Bool - - @TweakProperty(fallbackValue: false, - feature: Features.UICustomization, - variable: Variables.DisplayRedView, - tweakManager: tweakManager) - var canShowRedView: Bool - - @TweakProperty(fallbackValue: false, - feature: Features.UICustomization, - variable: Variables.DisplayGreenView, - tweakManager: tweakManager) - var canShowGreenView: Bool - - @TweakProperty(fallbackValue: "", - feature: Features.UICustomization, - variable: Variables.LabelText, - tweakManager: tweakManager) - var labelText: String - - @OptionalTweakProperty(fallbackValue: nil, - feature: Features.UICustomization, - variable: Variables.MeaningOfLife, - tweakManager: tweakManager) - var meaningOfLife: Int? - - // MARK: - Via TweakManager - - var canShowYellowView: Bool { - return tweakManager.tweakWith(feature: Features.UICustomization, - variable: Variables.DisplayYellowView)?.boolValue ?? false - } - - var redViewAlpha: Float { - return tweakManager.tweakWith(feature: Features.UICustomization, - variable: Variables.RedViewAlpha)?.floatValue ?? 0.0 - } - - var isTapGestureToChangeColorEnabled: Bool { - return tweakManager.tweakWith(feature: Features.General, - variable: Variables.TapToChangeViewColor)?.boolValue ?? false - } -} diff --git a/Example/JustTweak/Protocols/ExampleProtocol.swift b/Example/JustTweak/Protocols/ExampleProtocol.swift new file mode 100644 index 0000000..89f3a71 --- /dev/null +++ b/Example/JustTweak/Protocols/ExampleProtocol.swift @@ -0,0 +1,17 @@ +// +// GeneratedConfigurationAccessor+ExampleProtocol.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +protocol ExampleProtocol { + var meaningOfLife: Int { get } + var shouldShowAlert: Bool { get } + var isTapGestureToChangeColorEnabled: Bool { get } + var canShowGreenView: Bool { get } + var canShowRedView: Bool { get } + var canShowYellowView: Bool { get } + var labelText: String { get } + var redViewAlpha: Double { get } +} diff --git a/Example/JustTweak/Configurations/FirebaseConfiguration.swift b/Example/JustTweak/TweakProviders/FirebaseTweakProvider.swift similarity index 80% rename from Example/JustTweak/Configurations/FirebaseConfiguration.swift rename to Example/JustTweak/TweakProviders/FirebaseTweakProvider.swift index 5736e3f..a798a52 100644 --- a/Example/JustTweak/Configurations/FirebaseConfiguration.swift +++ b/Example/JustTweak/TweakProviders/FirebaseTweakProvider.swift @@ -1,5 +1,5 @@ // -// FirebaseConfiguration +// FirebaseTweakProvider.swift // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved. // @@ -7,7 +7,7 @@ import JustTweak import FirebaseCore import FirebaseRemoteConfig -public class FirebaseConfiguration: Configuration { +public class FirebaseTweakProvider: Configuration { public init() { /* DOWNLOAD THE GoogleService.plist from the Firebase dashboard */ @@ -37,14 +37,14 @@ public class FirebaseConfiguration: Configuration { guard configured else { return } remoteConfiguration.configSettings = RemoteConfigSettings() remoteConfiguration.fetch { [weak self] (status, error) in - guard let strongSelf = self else { return } + guard let self = self else { return } if let error = error { - strongSelf.logClosure?("Error while fetching Firebase configuration => \(error)", .error) + self.logClosure?("Error while fetching Firebase configuration => \(error)", .error) } else { - self?.remoteConfiguration.activate(completionHandler: 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 + 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 let notificationCentre = NotificationCenter.default - notificationCentre.post(name: TweakConfigurationDidChangeNotification, object: self) + notificationCentre.post(name: TweakProviderDidChangeNotification, object: self) } } } diff --git a/Example/JustTweak/TweakProviders/LocalTweaks_TopPriority_example.json b/Example/JustTweak/TweakProviders/LocalTweaks_TopPriority_example.json new file mode 100644 index 0000000..9261c53 --- /dev/null +++ b/Example/JustTweak/TweakProviders/LocalTweaks_TopPriority_example.json @@ -0,0 +1,31 @@ +{ + "ui_customization": { + "display_red_view": { + "Title": "Display Red View", + "Description": "shows a red view in the main view controller", + "Group": "UI Customization", + "Value": true + }, + "display_yellow_view": { + "Title": "Display Yellow View", + "Description": "shows a yellow view in the main view controller", + "Group": "UI Customization", + "Value": true + }, + "display_green_view": { + "Title": "Display Green View", + "Description": "shows a green view in the main view controller", + "Group": "UI Customization", + "Value": false + } + }, + "general": { + "answer_to_the_universe": { + "Title": "Definitive answer", + "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General", + "Value": 42, + "GeneratedPropertyName": "definitiveAnswer" + } + } +} diff --git a/Example/JustTweak/TweakProviders/LocalTweaks_example.json b/Example/JustTweak/TweakProviders/LocalTweaks_example.json new file mode 100644 index 0000000..25b12f7 --- /dev/null +++ b/Example/JustTweak/TweakProviders/LocalTweaks_example.json @@ -0,0 +1,61 @@ +{ + "ui_customization": { + "display_red_view": { + "Title": "Display Red View", + "Description": "shows a red view in the main view controller", + "Group": "UI Customization", + "Value": false, + "GeneratedPropertyName": "canShowRedView" + }, + "display_yellow_view": { + "Title": "Display Yellow View", + "Description": "shows a yellow view in the main view controller", + "Group": "UI Customization", + "Value": false, + "GeneratedPropertyName": "canShowYellowView" + }, + "display_green_view": { + "Title": "Display Green View", + "Description": "shows a green view in the main view controller", + "Group": "UI Customization", + "Value": true, + "GeneratedPropertyName": "canShowGreenView" + }, + "red_view_alpha_component": { + "Title": "Red View Alpha Component", + "Description": "defines the alpha level of the red view", + "Group": "UI Customization", + "Value": 1.0, + "GeneratedPropertyName": "redViewAlpha" + }, + "label_text": { + "Title": "Label Text", + "Description": "the title of the main label", + "Group": "UI Customization", + "Value": "Test value" + } + }, + "general": { + "answer_to_the_universe": { + "Title": "Definitive answer", + "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General", + "Value": 42, + "GeneratedPropertyName": "meaningOfLife" + }, + "greet_on_app_did_become_active": { + "Title": "Greet on app launch", + "Description": "shows an alert on applicationDidBecomeActive", + "Group": "General", + "Value": false, + "GeneratedPropertyName": "shouldShowAlert" + }, + "tap_to_change_color_enabled": { + "Title": "Tap to change views color", + "Description": "change the colour of the main view when receiving a tap", + "Group": "General", + "Value": true, + "GeneratedPropertyName": "isTapGestureToChangeColorEnabled" + } + } +} diff --git a/Example/JustTweak/Configurations/OptimizelyConfiguration.swift b/Example/JustTweak/TweakProviders/OptimizelyTweakProvider.swift similarity index 94% rename from Example/JustTweak/Configurations/OptimizelyConfiguration.swift rename to Example/JustTweak/TweakProviders/OptimizelyTweakProvider.swift index 8379550..ec24486 100644 --- a/Example/JustTweak/Configurations/OptimizelyConfiguration.swift +++ b/Example/JustTweak/TweakProviders/OptimizelyTweakProvider.swift @@ -1,12 +1,12 @@ // -// OptimizelyConfiguration.swift +// OptimizelyTweakProvider.swift // Copyright (c) 2018 Just Eat Holding Ltd. All rights reserved. // import JustTweak import OptimizelySDKiOS -public class OptimizelyConfiguration: Configuration { +public class OptimizelyTweakProvider: Configuration { private var optimizelyManager: OPTLYManager? private var optimizelyClient: OPTLYClient? @@ -32,7 +32,7 @@ public class OptimizelyConfiguration: Configuration { case (nil, let client): strongSelf.optimizelyClient = client let notificationCentre = NotificationCenter.default - notificationCentre.post(name: TweakConfigurationDidChangeNotification, object: strongSelf) + notificationCentre.post(name: TweakProviderDidChangeNotification, object: strongSelf) case (let error, _): if let error = error { strongSelf.logClosure?("Couldn't initialize Optimizely manager. \(error.localizedDescription)", .error) diff --git a/Example/JustTweak/Base.lproj/LaunchScreen.xib b/Example/JustTweak/UI/Base.lproj/LaunchScreen.xib similarity index 100% rename from Example/JustTweak/Base.lproj/LaunchScreen.xib rename to Example/JustTweak/UI/Base.lproj/LaunchScreen.xib diff --git a/Example/JustTweak/Base.lproj/Main.storyboard b/Example/JustTweak/UI/Base.lproj/Main.storyboard similarity index 100% rename from Example/JustTweak/Base.lproj/Main.storyboard rename to Example/JustTweak/UI/Base.lproj/Main.storyboard diff --git a/Example/Podfile b/Example/Podfile index 6cbacef..caf2a34 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -5,8 +5,15 @@ use_frameworks! target 'JustTweak_Example' do pod 'JustTweak', :path => '../' - pod 'Firebase/RemoteConfig' - pod 'OptimizelySDKiOS' + script_phase :name => 'TweakAccessorGenerator', + :script => '$SRCROOT/../JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator \ + -l $SRCROOT/JustTweak/TweakProviders/LocalTweaks_example.json \ + -o $SRCROOT/JustTweak/Accessors/ \ + -c $SRCROOT/JustTweak/CodeGeneration/', + :execution_position => :before_compile + +# pod 'Firebase/RemoteConfig' +# pod 'OptimizelySDKiOS' target 'JustTweak_Tests' do inherit! :search_paths diff --git a/Example/Podfile.lock b/Example/Podfile.lock index d2d7b7c..c329686 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,122 +1,16 @@ PODS: - - Firebase/CoreOnly (6.11.0): - - FirebaseCore (= 6.3.2) - - Firebase/RemoteConfig (6.11.0): - - Firebase/CoreOnly - - FirebaseRemoteConfig (~> 4.4.3) - - FirebaseABTesting (3.1.2): - - FirebaseAnalyticsInterop (~> 1.3) - - FirebaseCore (~> 6.1) - - Protobuf (>= 3.9.2, ~> 3.9) - - FirebaseAnalyticsInterop (1.4.0) - - FirebaseCore (6.3.2): - - FirebaseCoreDiagnostics (~> 1.0) - - FirebaseCoreDiagnosticsInterop (~> 1.0) - - GoogleUtilities/Environment (~> 6.2) - - GoogleUtilities/Logger (~> 6.2) - - FirebaseCoreDiagnostics (1.1.1): - - FirebaseCoreDiagnosticsInterop (~> 1.0) - - GoogleDataTransportCCTSupport (~> 1.0) - - GoogleUtilities/Environment (~> 6.2) - - GoogleUtilities/Logger (~> 6.2) - - nanopb (~> 0.3.901) - - FirebaseCoreDiagnosticsInterop (1.0.0) - - FirebaseInstanceID (4.2.6): - - FirebaseCore (~> 6.0) - - GoogleUtilities/Environment (~> 6.0) - - GoogleUtilities/UserDefaults (~> 6.0) - - FirebaseRemoteConfig (4.4.3): - - FirebaseABTesting (~> 3.1) - - FirebaseAnalyticsInterop (~> 1.4) - - FirebaseCore (~> 6.2) - - FirebaseInstanceID (~> 4.2) - - GoogleUtilities/Environment (~> 6.2) - - "GoogleUtilities/NSData+zlib (~> 6.2)" - - Protobuf (>= 3.9.2, ~> 3.9) - - GoogleDataTransport (3.0.1) - - GoogleDataTransportCCTSupport (1.2.1): - - GoogleDataTransport (~> 3.0) - - nanopb (~> 0.3.901) - - GoogleUtilities/Environment (6.3.1) - - GoogleUtilities/Logger (6.3.1): - - GoogleUtilities/Environment - - "GoogleUtilities/NSData+zlib (6.3.1)" - - GoogleUtilities/UserDefaults (6.3.1): - - GoogleUtilities/Logger - JustTweak (5.2.0) - - nanopb (0.3.9011): - - nanopb/decode (= 0.3.9011) - - nanopb/encode (= 0.3.9011) - - nanopb/decode (0.3.9011) - - nanopb/encode (0.3.9011) - - OptimizelySDKCore (3.1.3) - - OptimizelySDKDatafileManager (3.1.3): - - OptimizelySDKShared (= 3.1.3) - - OptimizelySDKEventDispatcher (3.1.3): - - OptimizelySDKShared (= 3.1.3) - - OptimizelySDKiOS (3.1.3): - - OptimizelySDKDatafileManager (= 3.1.3) - - OptimizelySDKEventDispatcher (= 3.1.3) - - OptimizelySDKUserProfileService (= 3.1.3) - - OptimizelySDKShared (3.1.3): - - OptimizelySDKCore (= 3.1.3) - - OptimizelySDKUserProfileService (3.1.3): - - OptimizelySDKShared (= 3.1.3) - - Protobuf (3.10.0) DEPENDENCIES: - - Firebase/RemoteConfig - JustTweak (from `../`) - - OptimizelySDKiOS - -SPEC REPOS: - https://github.com/CocoaPods/Specs.git: - - Firebase - - FirebaseABTesting - - FirebaseAnalyticsInterop - - FirebaseCore - - FirebaseCoreDiagnostics - - FirebaseCoreDiagnosticsInterop - - FirebaseInstanceID - - FirebaseRemoteConfig - - GoogleDataTransport - - GoogleDataTransportCCTSupport - - GoogleUtilities - - nanopb - - OptimizelySDKCore - - OptimizelySDKDatafileManager - - OptimizelySDKEventDispatcher - - OptimizelySDKiOS - - OptimizelySDKShared - - OptimizelySDKUserProfileService - - Protobuf EXTERNAL SOURCES: JustTweak: :path: "../" SPEC CHECKSUMS: - Firebase: bc9cfc7a96c73268656d5aaab453ff1b4b530e0e - FirebaseABTesting: 0d10f3cdc3fa00f3f175b5b56c1003c8e888299f - FirebaseAnalyticsInterop: d48b6ab67bcf016a05e55b71fc39c61c0cb6b7f3 - FirebaseCore: beeff42c07c30ea94702471d99db2089b594fbbd - FirebaseCoreDiagnostics: af29e43048607588c050889d19204f4d7b758c9f - FirebaseCoreDiagnosticsInterop: 6829da2b8d1fc795ff1bd99df751d3788035d2cb - FirebaseInstanceID: d0eafcd8bdbd3447cd694594734078c3e3e77d8b - FirebaseRemoteConfig: 51df936d5dfba17f267b2344070c89e8daa3fcfb - GoogleDataTransport: 166f9b9f82cbf60a204e8fe2daa9db3e3ec1fb15 - GoogleDataTransportCCTSupport: f6ab1962e9dc05ab1fb938b795e5b310209edeec - GoogleUtilities: f895fde57977df4e0233edda0dbeac490e3703b6 - JustTweak: dc29c5ba966b506c18e566c52095106d36c005ef - nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd - OptimizelySDKCore: 3f6018cc222b23de13ae9a71c2c15cd7fa397aed - OptimizelySDKDatafileManager: 34edeb189ef170992677149460172ef44bfa7250 - OptimizelySDKEventDispatcher: 081f55ea7cea340c8785c360734a132e889ed257 - OptimizelySDKiOS: b40202fc2aa6bc49ccc36edd9e3c04aae113683d - OptimizelySDKShared: e92e0d7ae787097bc146899ea24415b82e0a3e64 - OptimizelySDKUserProfileService: 5f4bd743a15dc10ba53eddf41b87321a60cbda00 - Protobuf: a4dc852ad69c027ca2166ed287b856697814375b + JustTweak: 8eb98832bb45457c962a63d1f33e3e025e4c57ea -PODFILE CHECKSUM: a7b727d37e751c9d933eb79a0171bbf9b553766d +PODFILE CHECKSUM: 5f9cf8c04d255b89415660583cf1d2a5b4c5a5ec COCOAPODS: 1.8.4 diff --git a/Example/Shared/Symbols.swift b/Example/Shared/Symbols.swift index 7fcf406..028cb96 100644 --- a/Example/Shared/Symbols.swift +++ b/Example/Shared/Symbols.swift @@ -7,17 +7,18 @@ import UIKit import JustTweak struct Features { - static let UICustomization = "ui_customization" - static let General = "general" + static let uiCustomization = "ui_customization" + static let general = "general" } struct Variables { - static let RedViewAlpha = "red_view_alpha_component" - static let DisplayRedView = "display_red_view" - static let DisplayGreenView = "display_green_view" - static let DisplayYellowView = "display_yellow_view" - static let TapToChangeViewColor = "tap_to_change_color_enabled" - static let LabelText = "label_text" - static let GreetOnAppDidBecomeActive = "greet_on_app_did_become_active" - static let MeaningOfLife = "meaning_of_life" + static let redViewAlpha = "red_view_alpha_component" + static let displayRedView = "display_red_view" + static let displayGreenView = "display_green_view" + static let displayYellowView = "display_yellow_view" + static let tapToChangeViewColor = "tap_to_change_color_enabled" + static let labelText = "label_text" + static let greetOnAppDidBecomeActive = "greet_on_app_did_become_active" + static let answerToTheUniverse = "answer_to_the_universe" + static let meaningOfLife = "meaning_of_life" } diff --git a/Example/Tests/Core/LocalConfigurationTests.swift b/Example/Tests/Core/LocalConfigurationTests.swift deleted file mode 100644 index 48d2cf8..0000000 --- a/Example/Tests/Core/LocalConfigurationTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// LocalConfigurationTests.swift -// Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved. -// - -import XCTest -import JustTweak - -class LocalConfigurationTests: XCTestCase { - - var configuration: LocalConfiguration! - - override func setUp() { - super.setUp() - configuration = configurationWithFileNamed("test_configuration")! - } - - override func tearDown() { - configuration = nil - super.tearDown() - } - - private func configurationWithFileNamed(_ fileName: String) -> LocalConfiguration? { - let bundle = Bundle(for: LocalConfigurationTests.self) - let jsonURL = bundle.url(forResource: fileName, withExtension: "json")! - return LocalConfiguration(jsonURL: jsonURL) - } - - func testParsesBoolTweak() { - let redViewTweak = Tweak(feature: Features.UICustomization, variable: Variables.DisplayRedView, value: true, title: "Display Red View", group: "UI Customization") - XCTAssertEqual(redViewTweak, configuration.tweakWith(feature: Features.UICustomization, variable: Variables.DisplayRedView)) - } - - func testParsesFloatTweak() { - let redViewAlphaTweak = Tweak(feature: Features.UICustomization, variable: Variables.RedViewAlpha, value: 1.0, title: "Red View Alpha Component", group: "UI Customization") - XCTAssertEqual(redViewAlphaTweak, configuration.tweakWith(feature: Features.UICustomization, variable: Variables.RedViewAlpha)) - } - - func testParsesStringTweak() { - let buttonLabelTweak = Tweak(feature: Features.UICustomization, variable: Variables.LabelText, value: "Test value", title: "Label Text", group: "UI Customization") - XCTAssertEqual(buttonLabelTweak, configuration.tweakWith(feature: Features.UICustomization, variable: Variables.LabelText)) - } -} diff --git a/Example/Tests/Core/LocalTweakProviderTests.swift b/Example/Tests/Core/LocalTweakProviderTests.swift new file mode 100644 index 0000000..1d406ea --- /dev/null +++ b/Example/Tests/Core/LocalTweakProviderTests.swift @@ -0,0 +1,57 @@ +// +// LocalTweakProviderTests.swift +// Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved. +// + +import XCTest +import JustTweak + +class LocalTweakProviderTests: XCTestCase { + + var tweakProvider: LocalTweakProvider! + + override func setUp() { + super.setUp() + tweakProvider = tweakProviderWithFileNamed("LocalTweaks_test")! + } + + override func tearDown() { + tweakProvider = nil + super.tearDown() + } + + private func tweakProviderWithFileNamed(_ fileName: String) -> LocalTweakProvider? { + let bundle = Bundle(for: LocalTweakProviderTests.self) + let jsonURL = bundle.url(forResource: fileName, withExtension: "json")! + return LocalTweakProvider(jsonURL: jsonURL) + } + + func testParsesBoolTweak() { + let redViewTweak = Tweak(feature: Features.uiCustomization, + variable: Variables.displayRedView, + value: true, + title: "Display Red View", + group: "UI Customization") + XCTAssertEqual(redViewTweak, tweakProvider.tweakWith(feature: Features.uiCustomization, + variable: Variables.displayRedView)) + } + + func testParsesFloatTweak() { + let redViewAlphaTweak = Tweak(feature: Features.uiCustomization, + variable: Variables.redViewAlpha, + value: 1.0, + title: "Red View Alpha Component", + group: "UI Customization") + XCTAssertEqual(redViewAlphaTweak, tweakProvider.tweakWith(feature: Features.uiCustomization, + variable: Variables.redViewAlpha)) + } + + func testParsesStringTweak() { + let buttonLabelTweak = Tweak(feature: Features.uiCustomization, + variable: Variables.labelText, + value: "Test value", + title: "Label Text", group: "UI Customization") + XCTAssertEqual(buttonLabelTweak, tweakProvider.tweakWith(feature: Features.uiCustomization, + variable: Variables.labelText)) + } +} diff --git a/Example/Tests/Core/PropertyWrappersTests.swift b/Example/Tests/Core/PropertyWrappersTests.swift index c9624b9..a1911c4 100644 --- a/Example/Tests/Core/PropertyWrappersTests.swift +++ b/Example/Tests/Core/PropertyWrappersTests.swift @@ -9,38 +9,38 @@ import JustTweak class Accessor { static let tweakManager: TweakManager = { - let ephemeralConfiguration: MutableConfiguration = NSMutableDictionary() - return TweakManager(configurations: [ephemeralConfiguration]) + let ephemeralTweakProvider: MutableTweakProvider = NSMutableDictionary() + return TweakManager(tweakProviders: [ephemeralTweakProvider]) }() - @TweakProperty(fallbackValue: "default", - feature: "stringValue_feature", - variable: "stringValue_variable", - tweakManager: tweakManager) + @FallbackTweakProperty(fallbackValue: "default", + feature: "stringValue_feature", + variable: "stringValue_variable", + tweakManager: tweakManager) var stringValue: String - @TweakProperty(fallbackValue: false, - feature: "boolValue_feature", - variable: "boolValue_variable", - tweakManager: tweakManager) + @FallbackTweakProperty(fallbackValue: false, + feature: "boolValue_feature", + variable: "boolValue_variable", + tweakManager: tweakManager) var boolValue: Bool - @TweakProperty(fallbackValue: 42, - feature: "intValue_feature", - variable: "intValue_variable", - tweakManager: tweakManager) + @FallbackTweakProperty(fallbackValue: 42, + feature: "intValue_feature", + variable: "intValue_variable", + tweakManager: tweakManager) var intValue: Int - @TweakProperty(fallbackValue: 3.14, - feature: "doubleValue_feature", - variable: "doubleValue_variable", - tweakManager: tweakManager) + @FallbackTweakProperty(fallbackValue: 3.14, + feature: "doubleValue_feature", + variable: "doubleValue_variable", + tweakManager: tweakManager) var doubleValue: Double - @TweakProperty(fallbackValue: 108.0, - feature: "floatValue_feature", - variable: "floatValue_variable", - tweakManager: tweakManager) + @FallbackTweakProperty(fallbackValue: 108.0, + feature: "floatValue_feature", + variable: "floatValue_variable", + tweakManager: tweakManager) var floatValue: Float @OptionalTweakProperty(fallbackValue: nil, diff --git a/Example/Tests/Core/TweakManager+PresentationTests.swift b/Example/Tests/Core/TweakManager+PresentationTests.swift index 411494d..6456c14 100644 --- a/Example/Tests/Core/TweakManager+PresentationTests.swift +++ b/Example/Tests/Core/TweakManager+PresentationTests.swift @@ -12,20 +12,20 @@ import XCTest class TweakManager_PresentationTests: XCTestCase { var tweakManager: TweakManager! - let localConfigurationLowPriority: LocalConfiguration = { + let localTweakProviderLowPriority: LocalTweakProvider = { let bundle = Bundle(for: TweakManagerTests.self) - let jsonConfigurationURL = bundle.url(forResource: "test_configuration", withExtension: "json")! - return LocalConfiguration(jsonURL: jsonConfigurationURL) + let jsonConfigurationURL = bundle.url(forResource: "LocalTweaks_test", withExtension: "json")! + return LocalTweakProvider(jsonURL: jsonConfigurationURL) }() - let localConfigurationHighPriority: LocalConfiguration = { + let localTweakProviderHighPriority: LocalTweakProvider = { let bundle = Bundle(for: TweakManagerTests.self) - let jsonConfigurationURL = bundle.url(forResource: "test_configuration_override", withExtension: "json")! - return LocalConfiguration(jsonURL: jsonConfigurationURL) + let jsonConfigurationURL = bundle.url(forResource: "LocalTweaks_test_override", withExtension: "json")! + return LocalTweakProvider(jsonURL: jsonConfigurationURL) }() - func test_GivenOneLocalConfiguration_WhenFetchedDisplayableTweaks_ThenAllTweaksSortedByTitleAreReturned() { - let configurations: [Configuration] = [localConfigurationLowPriority] - tweakManager = TweakManager(configurations: configurations) + func test_GivenOneLocalTweakProvider_WhenFetchedDisplayableTweaks_ThenAllTweaksSortedByTitleAreReturned() { + let tweakProviders: [TweakProvider] = [localTweakProviderLowPriority] + tweakManager = TweakManager(tweakProviders: tweakProviders) let displayableTweaks = tweakManager.displayableTweaks let targetTweaks = [ Tweak(feature: "ui_customization", @@ -67,9 +67,9 @@ class TweakManager_PresentationTests: XCTestCase { XCTAssertEqual(displayableTweaks, targetTweaks) } - func test_GivenTwoLocalConfigurations_WhenFetchedDisplayableTweaks_ThenTweaksFromBothConfigurationsSortedByTitleAreReturned() { - let configurations: [Configuration] = [localConfigurationHighPriority, localConfigurationLowPriority] - tweakManager = TweakManager(configurations: configurations) + func test_GivenTwoLocalTweakProviders_WhenFetchedDisplayableTweaks_ThenTweaksFromBothConfigurationsSortedByTitleAreReturned() { + let tweakProviders: [LocalTweakProvider] = [localTweakProviderHighPriority, localTweakProviderLowPriority] + tweakManager = TweakManager(tweakProviders: tweakProviders) let displayableTweaks = tweakManager.displayableTweaks let targetTweaks = [ Tweak(feature: "ui_customization", diff --git a/Example/Tests/Core/TweakManagerCacheTests.swift b/Example/Tests/Core/TweakManagerCacheTests.swift index 2b10a69..6ee587c 100644 --- a/Example/Tests/Core/TweakManagerCacheTests.swift +++ b/Example/Tests/Core/TweakManagerCacheTests.swift @@ -16,18 +16,18 @@ fileprivate struct Constants { class TweakManagerCacheTests: XCTestCase { - fileprivate var mockConfiguration: MockConfiguration! + fileprivate var mockTweakProvider: MockTweakProvider! var tweakManager: TweakManager! override func setUp() { super.setUp() - mockConfiguration = MockConfiguration() - tweakManager = TweakManager(configurations: [mockConfiguration]) + mockTweakProvider = MockTweakProvider() + tweakManager = TweakManager(tweakProviders: [mockTweakProvider]) tweakManager.useCache = true } override func tearDown() { - mockConfiguration = nil + mockTweakProvider = nil tweakManager = nil super.tearDown() } @@ -44,14 +44,14 @@ class TweakManagerCacheTests: XCTestCase { private func testFeatureEnabled(useCache: Bool) { tweakManager.useCache = useCache - XCTAssertEqual(mockConfiguration.isFeatureEnabledCallsCounter, 0) + XCTAssertEqual(mockTweakProvider.isFeatureEnabledCallsCounter, 0) XCTAssertEqual(tweakManager.isFeatureEnabled(Constants.feature), Constants.featureActiveValue) - XCTAssertEqual(mockConfiguration.isFeatureEnabledCallsCounter, 1) + XCTAssertEqual(mockTweakProvider.isFeatureEnabledCallsCounter, 1) XCTAssertEqual(tweakManager.isFeatureEnabled(Constants.feature), Constants.featureActiveValue) - XCTAssertEqual(mockConfiguration.isFeatureEnabledCallsCounter, useCache ? 1 : 2) + XCTAssertEqual(mockTweakProvider.isFeatureEnabledCallsCounter, useCache ? 1 : 2) tweakManager.resetCache() XCTAssertEqual(tweakManager.isFeatureEnabled(Constants.feature), Constants.featureActiveValue) - XCTAssertEqual(mockConfiguration.isFeatureEnabledCallsCounter, useCache ? 2 : 3) + XCTAssertEqual(mockTweakProvider.isFeatureEnabledCallsCounter, useCache ? 2 : 3) } // MARK: - tweakWith(feature:variable:) @@ -66,19 +66,19 @@ class TweakManagerCacheTests: XCTestCase { private func tweakFetch(useCache: Bool) { tweakManager.useCache = useCache - XCTAssertEqual(mockConfiguration.tweakWithFeatureVariableCallsCounter, 0) + XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, 0) let value = true tweakManager.set(value, feature: Constants.feature, variable: Constants.variable) XCTAssertEqual(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)!.value as! Bool, value) - XCTAssertEqual(mockConfiguration.tweakWithFeatureVariableCallsCounter, 1) + XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, 1) XCTAssertEqual(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)!.value as! Bool, value) - XCTAssertEqual(mockConfiguration.tweakWithFeatureVariableCallsCounter, useCache ? 1 : 2) + XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, useCache ? 1 : 2) tweakManager.set(value, feature: Constants.feature, variable: Constants.variable) XCTAssertEqual(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)!.value as! Bool, value) - XCTAssertEqual(mockConfiguration.tweakWithFeatureVariableCallsCounter, useCache ? 2 : 3) + XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, useCache ? 2 : 3) tweakManager.resetCache() XCTAssertEqual(tweakManager.tweakWith(feature: Constants.feature, variable: Constants.variable)!.value as! Bool, value) - XCTAssertEqual(mockConfiguration.tweakWithFeatureVariableCallsCounter, useCache ? 3 : 4) + XCTAssertEqual(mockTweakProvider.tweakWithFeatureVariableCallsCounter, useCache ? 3 : 4) } // MARK: - activeVariation(experiment:) @@ -93,18 +93,18 @@ class TweakManagerCacheTests: XCTestCase { private func testActiveVariation(useCache: Bool) { tweakManager.useCache = useCache - XCTAssertEqual(mockConfiguration.activeVariationCallsCounter, 0) + XCTAssertEqual(mockTweakProvider.activeVariationCallsCounter, 0) XCTAssertEqual(tweakManager.activeVariation(for: Constants.experiment), Constants.activeVariation) - XCTAssertEqual(mockConfiguration.activeVariationCallsCounter, 1) + XCTAssertEqual(mockTweakProvider.activeVariationCallsCounter, 1) XCTAssertEqual(tweakManager.activeVariation(for: Constants.experiment), Constants.activeVariation) - XCTAssertEqual(mockConfiguration.activeVariationCallsCounter, useCache ? 1 : 2) + XCTAssertEqual(mockTweakProvider.activeVariationCallsCounter, useCache ? 1 : 2) tweakManager.resetCache() XCTAssertEqual(tweakManager.activeVariation(for: Constants.experiment), Constants.activeVariation) - XCTAssertEqual(mockConfiguration.activeVariationCallsCounter, useCache ? 2 : 3) + XCTAssertEqual(mockTweakProvider.activeVariationCallsCounter, useCache ? 2 : 3) } } -fileprivate class MockConfiguration: MutableConfiguration { +fileprivate class MockTweakProvider: MutableTweakProvider { var logClosure: LogClosure? diff --git a/Example/Tests/Core/TweakManagerTests.swift b/Example/Tests/Core/TweakManagerTests.swift index be5315f..7a1ef35 100644 --- a/Example/Tests/Core/TweakManagerTests.swift +++ b/Example/Tests/Core/TweakManagerTests.swift @@ -9,61 +9,61 @@ import XCTest class TweakManagerTests: XCTestCase { var tweakManager: TweakManager! - let localConfiguration: LocalConfiguration = { + let localTweakProvider: TweakProvider = { let bundle = Bundle(for: TweakManagerTests.self) - let jsonConfigurationURL = bundle.url(forResource: "test_configuration", withExtension: "json")! - return LocalConfiguration(jsonURL: jsonConfigurationURL) + let jsonConfigurationURL = bundle.url(forResource: "LocalTweaks_test", withExtension: "json")! + return LocalTweakProvider(jsonURL: jsonConfigurationURL) }() - var userDefaultsConfiguration: UserDefaultsConfiguration! + var userDefaultsTweakProvider: UserDefaultsTweakProvider! override func setUp() { super.setUp() - let mockConfiguration = MockConfiguration() + let mockTweakProvider = MockTweakProvider() let testUserDefaults = UserDefaults(suiteName: "com.JustTweak.TweakManagerTests")! - userDefaultsConfiguration = UserDefaultsConfiguration(userDefaults: testUserDefaults) - let configurations: [Configuration] = [userDefaultsConfiguration, mockConfiguration, localConfiguration] - tweakManager = TweakManager(configurations: configurations) + userDefaultsTweakProvider = UserDefaultsTweakProvider(userDefaults: testUserDefaults) + let tweakProviders: [TweakProvider] = [userDefaultsTweakProvider, mockTweakProvider, localTweakProvider] + tweakManager = TweakManager(tweakProviders: tweakProviders) } override func tearDown() { - userDefaultsConfiguration.deleteValue(feature: Features.UICustomization, variable: Variables.GreetOnAppDidBecomeActive) + userDefaultsTweakProvider.deleteValue(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive) tweakManager = nil super.tearDown() } - func testReturnsNoMutableConfiguration_IfNoneHasBeenPassedToInitializer() { - let tweakManager = TweakManager(configurations: [localConfiguration]) - XCTAssertNil(tweakManager.mutableConfiguration) + func testReturnsNoMutableTweakProvider_IfNoneHasBeenPassedToInitializer() { + let tweakManager = TweakManager(tweakProviders: [localTweakProvider]) + XCTAssertNil(tweakManager.mutableTweakProvider) } func testReturnsNil_ForUndefinedTweak() { - XCTAssertNil(tweakManager.tweakWith(feature: Features.UICustomization, variable: "some_undefined_tweak")) + XCTAssertNil(tweakManager.tweakWith(feature: Features.uiCustomization, variable: "some_undefined_tweak")) } func testReturnsRemoteConfigValue_ForDisplayRedViewTweak() { - XCTAssertTrue(tweakManager.tweakWith(feature: Features.UICustomization, variable: Variables.DisplayRedView)!.boolValue) + XCTAssertTrue(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView)!.boolValue) } func testReturnsRemoteConfigValue_ForDisplayYellowViewTweak() { - XCTAssertFalse(tweakManager.tweakWith(feature: Features.UICustomization, variable: Variables.DisplayYellowView)!.boolValue) + XCTAssertFalse(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView)!.boolValue) } func testReturnsRemoteConfigValue_ForDisplayGreenViewTweak() { - XCTAssertFalse(tweakManager.tweakWith(feature: Features.UICustomization, variable: Variables.DisplayGreenView)!.boolValue) + XCTAssertFalse(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayGreenView)!.boolValue) } func testReturnsRemoteConfigValue_ForGreetOnAppDidBecomeActiveTweak() { - XCTAssertTrue(tweakManager.tweakWith(feature: Features.UICustomization, variable: Variables.GreetOnAppDidBecomeActive)!.boolValue) + XCTAssertTrue(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)!.boolValue) } func testReturnsJSONConfigValue_ForTapToChangeViewColorTweak_AsYetUnkown() { - XCTAssertTrue(tweakManager.tweakWith(feature: Features.General, variable: Variables.TapToChangeViewColor)!.boolValue) + XCTAssertTrue(tweakManager.tweakWith(feature: Features.general, variable: Variables.tapToChangeViewColor)!.boolValue) } - func testReturnsUserSetValue_ForGreetOnAppDidBecomeActiveTweak_AfterUpdatingUserDefaultsConfiguration() { - let mutableConfiguration = tweakManager.mutableConfiguration! - mutableConfiguration.set(false, feature: Features.UICustomization, variable: Variables.GreetOnAppDidBecomeActive) - XCTAssertFalse(tweakManager.tweakWith(feature: Features.UICustomization, variable: Variables.GreetOnAppDidBecomeActive)!.boolValue) + func testReturnsUserSetValue_ForGreetOnAppDidBecomeActiveTweak_AfterUpdatingUserDefaultsTweakProvider() { + let mutableTweakProvider = tweakManager.mutableTweakProvider! + mutableTweakProvider.set(false, feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive) + XCTAssertFalse(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.greetOnAppDidBecomeActive)!.boolValue) } func testCallsClosureForRegisteredObserverWhenAnyConfigurationChanges() { @@ -72,8 +72,8 @@ class TweakManagerTests: XCTestCase { didCallClosure = true } let tweak = Tweak(feature: "feature", variable: "variable", value: "value") - let userInfo = [TweakConfigurationDidChangeNotificationTweakKey: tweak] - NotificationCenter.default.post(name: TweakConfigurationDidChangeNotification, object: self, userInfo: userInfo) + let userInfo = [TweakProviderDidChangeNotificationTweakKey: tweak] + NotificationCenter.default.post(name: TweakProviderDidChangeNotification, object: self, userInfo: userInfo) XCTAssertTrue(didCallClosure) } @@ -84,20 +84,20 @@ class TweakManagerTests: XCTestCase { } tweakManager.deregisterFromConfigurationsUpdates(self) let tweak = Tweak(feature: "feature", variable: "variable", value: "value") - let userInfo = [TweakConfigurationDidChangeNotificationTweakKey: tweak] - NotificationCenter.default.post(name: TweakConfigurationDidChangeNotification, object: self, userInfo: userInfo) + let userInfo = [TweakProviderDidChangeNotificationTweakKey: tweak] + NotificationCenter.default.post(name: TweakProviderDidChangeNotification, object: self, userInfo: userInfo) XCTAssertFalse(didCallClosure) } } -fileprivate class MockConfiguration: Configuration { +fileprivate class MockTweakProvider: TweakProvider { var logClosure: LogClosure? let features: [String : [String]] = [:] - let knownValues = [Variables.DisplayRedView: ["Value": true], - Variables.DisplayYellowView: ["Value": false], - Variables.DisplayGreenView: ["Value": false], - Variables.GreetOnAppDidBecomeActive: ["Value": true]] + let knownValues = [Variables.displayRedView: ["Value": true], + Variables.displayYellowView: ["Value": false], + Variables.displayGreenView: ["Value": false], + Variables.greetOnAppDidBecomeActive: ["Value": true]] func isFeatureEnabled(_ feature: String) -> Bool { return false diff --git a/Example/Tests/Core/UserDefaultsTweaksConfigurationTests.swift b/Example/Tests/Core/UserDefaultsTweakProviderTests.swift similarity index 67% rename from Example/Tests/Core/UserDefaultsTweaksConfigurationTests.swift rename to Example/Tests/Core/UserDefaultsTweakProviderTests.swift index e23cbcb..5f54b77 100644 --- a/Example/Tests/Core/UserDefaultsTweaksConfigurationTests.swift +++ b/Example/Tests/Core/UserDefaultsTweakProviderTests.swift @@ -1,31 +1,31 @@ // -// ConfigurationAccessor.swift +// UserDefaultsTweakProviderTests.swift // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved. // import XCTest import JustTweak -class UserDefaultsConfigurationTests: XCTestCase { +class UserDefaultsTweakProviderTests: XCTestCase { - var userDefaultsConfiguration: UserDefaultsConfiguration! - let userDefaults = UserDefaults(suiteName: String(describing: UserDefaultsConfigurationTests.self))! + var userDefaultsTweakProvider: UserDefaultsTweakProvider! + let userDefaults = UserDefaults(suiteName: String(describing: UserDefaultsTweakProviderTests.self))! private let userDefaultsKeyPrefix = "lib.fragments.userDefaultsKey" override func setUp() { super.setUp() - userDefaultsConfiguration = UserDefaultsConfiguration(userDefaults: userDefaults) + userDefaultsTweakProvider = UserDefaultsTweakProvider(userDefaults: userDefaults) } override func tearDown() { userDefaults.removeObject(forKey: "\(userDefaultsKeyPrefix).display_red_view") - userDefaultsConfiguration = nil + userDefaultsTweakProvider = nil super.tearDown() } func testReturnsCorrectTweaksIdentifiersWhenInitializedAndTweaksHaveBeenSet() { - let anotherConfiguration = UserDefaultsConfiguration(userDefaults: userDefaults) + let anotherConfiguration = UserDefaultsTweakProvider(userDefaults: userDefaults) anotherConfiguration.set("hello", feature: "feature_1", variable: "variable_4") anotherConfiguration.set(true, feature: "feature_1", variable: "variable_3") anotherConfiguration.set(12.34, feature: "feature_1", variable: "variable_2") @@ -38,37 +38,37 @@ class UserDefaultsConfigurationTests: XCTestCase { } func testReturnsNilForTweaksThatHaveNoUserDefaultValue() { - let tweak = userDefaultsConfiguration.tweakWith(feature: Features.UICustomization, variable: Variables.DisplayRedView) + let tweak = userDefaultsTweakProvider.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView) XCTAssertNil(tweak) } func testUpdatesValueForTweak_withBool() { - userDefaultsConfiguration.set(true, feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsConfiguration.tweakWith(feature: "feature_1", variable: "variable_1") + userDefaultsTweakProvider.set(true, feature: "feature_1", variable: "variable_1") + let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") XCTAssertTrue(tweak!.value == true) } func testUpdatesValueForTweak_withInteger() { - userDefaultsConfiguration.set(42, feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsConfiguration.tweakWith(feature: "feature_1", variable: "variable_1") + userDefaultsTweakProvider.set(42, feature: "feature_1", variable: "variable_1") + let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") XCTAssertTrue(tweak!.value == 42) } func testUpdatesValueForTweak_withFloat() { - userDefaultsConfiguration.set(Float(12.34), feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsConfiguration.tweakWith(feature: "feature_1", variable: "variable_1") + userDefaultsTweakProvider.set(Float(12.34), feature: "feature_1", variable: "variable_1") + let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") XCTAssertTrue(tweak!.value == Float(12.34)) } func testUpdatesValueForTweak_withDouble() { - userDefaultsConfiguration.set(Double(23.45), feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsConfiguration.tweakWith(feature: "feature_1", variable: "variable_1") + userDefaultsTweakProvider.set(Double(23.45), feature: "feature_1", variable: "variable_1") + let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") XCTAssertTrue(tweak!.value == Double(23.45)) } func testUpdatesValueForTweak_withString() { - userDefaultsConfiguration.set("Hello", feature: "feature_1", variable: "variable_1") - let tweak = userDefaultsConfiguration.tweakWith(feature: "feature_1", variable: "variable_1") + userDefaultsTweakProvider.set("Hello", feature: "feature_1", variable: "variable_1") + let tweak = userDefaultsTweakProvider.tweakWith(feature: "feature_1", variable: "variable_1") XCTAssertTrue(tweak!.value == "Hello") } } diff --git a/Example/Tests/test_configuration.json b/Example/Tests/LocalTweaks_test.json similarity index 100% rename from Example/Tests/test_configuration.json rename to Example/Tests/LocalTweaks_test.json diff --git a/Example/Tests/test_configuration_override.json b/Example/Tests/LocalTweaks_test_override.json similarity index 100% rename from Example/Tests/test_configuration_override.json rename to Example/Tests/LocalTweaks_test_override.json diff --git a/Example/Tests/UI/TweakViewControllerTests.swift b/Example/Tests/UI/TweakViewControllerTests.swift index 08b5248..5416e4e 100644 --- a/Example/Tests/UI/TweakViewControllerTests.swift +++ b/Example/Tests/UI/TweakViewControllerTests.swift @@ -8,18 +8,43 @@ import XCTest class TweakViewControllerTests: XCTestCase { - var viewController: TweakViewController! - var tweakManager: TweakManager! + private var rootWindow: UIWindow! + private var viewController: TweakViewController! + private var tweakManager: TweakManager! override func setUp() { super.setUp() - buildViewControllerWithConfigurationFromFileNamed("test_configuration") + let bundle = Bundle(for: TweakViewControllerTests.self) + let jsonURL = bundle.url(forResource: "LocalTweaks_test", withExtension: "json") + let localTweakProvider = LocalTweakProvider(jsonURL: jsonURL!) + let userDefaults = UserDefaults(suiteName: "com.JustTweaks.Tests\(NSDate.timeIntervalSinceReferenceDate)")! + let userDefaultsTweakProvider = UserDefaultsTweakProvider(userDefaults: userDefaults) + let tweakProviders: [TweakProvider] = [userDefaultsTweakProvider, localTweakProvider] + tweakManager = TweakManager(tweakProviders: tweakProviders) + viewController = TweakViewController(style: .plain, tweakManager: tweakManager) + + rootWindow = UIWindow(frame: UIScreen.main.bounds) + rootWindow.makeKeyAndVisible() + rootWindow.isHidden = false + rootWindow.rootViewController = viewController + _ = viewController.view + viewController.viewWillAppear(false) + viewController.viewDidAppear(false) } override func tearDown() { - let mutableConfiguration = tweakManager.mutableConfiguration! - mutableConfiguration.deleteValue(feature: "feature_1", variable: "variable_1") + let mutableTweakProvider = tweakManager.mutableTweakProvider! + mutableTweakProvider.deleteValue(feature: "feature_1", variable: "variable_1") viewController = nil + + let rootViewController = rootWindow!.rootViewController! + rootViewController.viewWillDisappear(false) + rootViewController.viewDidDisappear(false) + rootWindow.rootViewController = nil + rootWindow.isHidden = true + self.rootWindow = nil + self.viewController = nil + super.tearDown() } @@ -39,19 +64,19 @@ class TweakViewControllerTests: XCTestCase { // MARK: Convenience Methods func testReturnsCorrectIndexPathForTweak_WhenTweakFound() { - let indexPath = viewController.indexPathForTweak(with: Features.UICustomization, variable: Variables.DisplayGreenView)! + let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayGreenView)! let expectedIndexPath = IndexPath(row: 0, section: 1) XCTAssertEqual(indexPath, expectedIndexPath) } func testReturnsCorrectIndexPathForTweak_WhenTweakFound_2() { - let indexPath = viewController.indexPathForTweak(with: Features.UICustomization, variable: Variables.DisplayRedView)! + let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayRedView)! let expectedIndexPath = IndexPath(row: 1, section: 1) XCTAssertEqual(indexPath, expectedIndexPath) } func testReturnsCorrectIndexPathForTweak_WhenTweakFound_3() { - let indexPath = viewController.indexPathForTweak(with: Features.UICustomization, variable: Variables.DisplayYellowView)! + let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayYellowView)! let expectedIndexPath = IndexPath(row: 2, section: 1) XCTAssertEqual(indexPath, expectedIndexPath) } @@ -59,38 +84,38 @@ class TweakViewControllerTests: XCTestCase { // MARK: Tweak Cells Display func testReturnsCorrectIndexPathForTweak_WhenTweakNotFound() { - let indexPath = viewController.indexPathForTweak(with: Features.General, variable: "some_nonexisting_tweak") + let indexPath = viewController.indexPathForTweak(with: Features.general, variable: "some_nonexisting_tweak") XCTAssertNil(indexPath) } func testDisplaysTweakOn_IfEnabled() { - let indexPath = viewController.indexPathForTweak(with: Features.UICustomization, variable: Variables.DisplayYellowView)! + let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayYellowView)! let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath) as! BooleanTweakTableViewCell XCTAssertFalse(cell.switchControl.isOn) } func testDisplaysTweakOff_IfDisabled() { - let indexPath = viewController.indexPathForTweak(with: Features.UICustomization, variable: Variables.DisplayRedView)! + let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayRedView)! let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath) as! BooleanTweakTableViewCell XCTAssertTrue(cell.switchControl.isOn) } func testDisplaysTweakTitle_ForTweakThatHaveIt() { - let indexPath = viewController.indexPathForTweak(with: Features.UICustomization, variable: Variables.DisplayRedView)! + let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayRedView)! let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath) XCTAssertEqual(cell.textLabel?.text, "Display Red View") XCTAssertEqual((cell as! TweakViewControllerCell).title, "Display Red View") } func testDisplaysNumericTweaksCorrectly() { - let indexPath = viewController.indexPathForTweak(with: Features.UICustomization, variable: Variables.RedViewAlpha)! + let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.redViewAlpha)! let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath) as? NumericTweakTableViewCell XCTAssertEqual(cell?.title, "Red View Alpha Component") XCTAssertEqual(cell?.textField.text, "1.0") } func testDisplaysTextTweaksCorrectly() { - let indexPath = viewController.indexPathForTweak(with: Features.UICustomization, variable: Variables.LabelText)! + let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.labelText)! let cell = viewController.tableView(viewController.tableView, cellForRowAt: indexPath) as? TextTweakTableViewCell XCTAssertEqual(cell?.title, "Label Text") XCTAssertEqual(cell?.textField.text, "Test value") @@ -99,15 +124,14 @@ class TweakViewControllerTests: XCTestCase { // MARK: Cells Actions func testUpdatesValueOfTweak_WhenUserTooglesSwitchOnBooleanCell() { - viewController.beginAppearanceTransition(true, animated: false) viewController.endAppearanceTransition() - let indexPath = viewController.indexPathForTweak(with: Features.UICustomization, variable: Variables.DisplayYellowView)! + let indexPath = viewController.indexPathForTweak(with: Features.uiCustomization, variable: Variables.displayYellowView)! let cell = viewController.tableView.cellForRow(at: indexPath) as! BooleanTweakTableViewCell cell.switchControl.isOn = true cell.switchControl.sendActions(for: .valueChanged) - XCTAssertTrue(tweakManager.tweakWith(feature: Features.UICustomization, variable: Variables.DisplayYellowView)!.boolValue) + XCTAssertTrue(tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayYellowView)!.boolValue) } // MARK: Other Actions @@ -125,17 +149,4 @@ class TweakViewControllerTests: XCTestCase { vc.dismissViewController() XCTAssertTrue(vc.mockPresentingViewController.didCallDismissal) } - - // MARK: Helpers - - private func buildViewControllerWithConfigurationFromFileNamed(_ fileName: String) { - let bundle = Bundle(for: TweakViewControllerTests.self) - let jsonURL = bundle.url(forResource: fileName, withExtension: "json") - let localConfiguration = LocalConfiguration(jsonURL: jsonURL!) - let userDefaults = UserDefaults(suiteName: "com.JustTweaks.Tests\(NSDate.timeIntervalSinceReferenceDate)")! - let userDefaultsConfiguration = UserDefaultsConfiguration(userDefaults: userDefaults) - let configurations: [Configuration] = [userDefaultsConfiguration, localConfiguration] - tweakManager = TweakManager(configurations: configurations) - viewController = TweakViewController(style: .plain, tweakManager: tweakManager) - } } diff --git a/Example/Tests/test_configuration_invalid.json b/Example/Tests/test_configuration_invalid.json deleted file mode 100644 index fe51488..0000000 --- a/Example/Tests/test_configuration_invalid.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/Gemfile.lock b/Gemfile.lock index 66693ea..16d2e9c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,13 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.1) - activesupport (4.2.11.1) + CFPropertyList (3.0.3) + activesupport (4.2.11.3) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - algoliasearch (1.27.1) + algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) @@ -38,17 +38,17 @@ GEM fuzzy_match (~> 2.0.4) nap (~> 1.0) cocoapods-deintegrate (1.0.4) - cocoapods-downloader (1.2.2) + cocoapods-downloader (1.4.0) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.0) cocoapods-stats (1.1.0) - cocoapods-trunk (1.4.1) + cocoapods-trunk (1.5.0) nap (>= 0.8, < 2.0) netrc (~> 0.11) - cocoapods-try (1.1.0) + cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.1.5) + concurrent-ruby (1.1.8) escape (0.0.4) fourflusher (2.3.1) fuzzy_match (2.0.4) @@ -56,25 +56,26 @@ GEM httpclient (2.8.3) i18n (0.9.5) concurrent-ruby (~> 1.0) - json (2.3.1) - minitest (5.12.2) + json (2.5.1) + minitest (5.14.4) molinillo (0.6.6) - nanaimo (0.2.6) + nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) ruby-macho (1.4.0) thread_safe (0.3.6) - tzinfo (1.2.5) + tzinfo (1.2.9) thread_safe (~> 0.1) - xcodeproj (1.13.0) + xcodeproj (1.19.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.2.6) + nanaimo (~> 0.3.0) PLATFORMS ruby + x86_64-darwin-18 DEPENDENCIES cocoapods (~> 1.8.4) @@ -83,4 +84,4 @@ RUBY VERSION ruby 2.5.3p105 BUNDLED WITH - 1.17.3 + 2.2.15 diff --git a/JustTweak.podspec b/JustTweak.podspec index 19c2fcf..cacd7c5 100644 --- a/JustTweak.podspec +++ b/JustTweak.podspec @@ -10,8 +10,9 @@ JustTweak is a framework for feature flagging, locally and remotely configure an s.homepage = 'https://github.com/justeat/JustTweak' s.license = { :type => 'Apache 2.0', :file => 'LICENSE' } s.authors = { 'Gianluca Tranchedone' => 'gianluca.tranchedone@just-eat.com', - 'Alberto De Bortoli' => 'alberto.debortoli@just-eat.com', - 'Dimitar Chakarov' => 'dimitar.chakarov@just-eat.com' } + 'Alberto De Bortoli' => 'alberto.debortoli@justeattakeaway.com', + 'Andrew Steven Grant' => 'andrew.grant@justeattakeaway.com', + 'Dimitar Chakarov' => 'dimitar.chakarov@justeattakeaway.com' } s.source = { :git => 'https://github.com/justeat/JustTweak.git', :tag => s.version.to_s } s.ios.deployment_target = '11.0' diff --git a/JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator b/JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator new file mode 100755 index 0000000..329b9cb Binary files /dev/null and b/JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator differ diff --git a/JustTweak/Classes/Protocols/Configurations.swift b/JustTweak/Classes/Protocols/TweakProvider.swift similarity index 62% rename from JustTweak/Classes/Protocols/Configurations.swift rename to JustTweak/Classes/Protocols/TweakProvider.swift index d5b2a37..066bb9c 100644 --- a/JustTweak/Classes/Protocols/Configurations.swift +++ b/JustTweak/Classes/Protocols/TweakProvider.swift @@ -1,5 +1,5 @@ // -// TweaksConfiguration.swift +// TweakProvider.swift // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved. // @@ -11,17 +11,17 @@ public enum LogLevel: Int { public typealias LogClosure = (String, LogLevel) -> Void -public protocol Configuration { +public protocol TweakProvider { var logClosure: LogClosure? { set get } func isFeatureEnabled(_ feature: String) -> Bool func tweakWith(feature: String, variable: String) -> Tweak? func activeVariation(for experiment: String) -> String? } -public protocol MutableConfiguration: Configuration { +public protocol MutableTweakProvider: TweakProvider { func set(_ value: TweakValue, feature: String, variable: String) func deleteValue(feature: String, variable: String) } -public let TweakConfigurationDidChangeNotification = Notification.Name("TweakConfigurationDidChangeNotification") -public let TweakConfigurationDidChangeNotificationTweakKey = "TweakConfigurationDidChangeNotificationTweakKey" +public let TweakProviderDidChangeNotification = Notification.Name("TweakProviderDidChangeNotification") +public let TweakProviderDidChangeNotificationTweakKey = "TweakProviderDidChangeNotificationTweakKey" diff --git a/JustTweak/Classes/TweakManager.swift b/JustTweak/Classes/TweakManager.swift index 9c9b929..2ceed54 100644 --- a/JustTweak/Classes/TweakManager.swift +++ b/JustTweak/Classes/TweakManager.swift @@ -7,12 +7,12 @@ import Foundation final public class TweakManager { - var configurations: [Configuration] + var tweakProviders: [TweakProvider] public var logClosure: LogClosure? { didSet { - for (index, _) in configurations.enumerated() { - configurations[index].logClosure = logClosure + for (index, _) in tweakProviders.enumerated() { + tweakProviders[index].logClosure = logClosure } } } @@ -32,17 +32,17 @@ final public class TweakManager { private var experimentCache = [String : String]() private var observersMap = [NSObject : NSObjectProtocol]() - var mutableConfiguration: MutableConfiguration? { - return configurations.first { $0 is MutableConfiguration } as? MutableConfiguration + var mutableTweakProvider: MutableTweakProvider? { + return tweakProviders.first { $0 is MutableTweakProvider } as? MutableTweakProvider } - public init(configurations: [Configuration]) { - self.configurations = configurations - for (index, _) in self.configurations.enumerated() { - self.configurations[index].logClosure = logClosure + public init(tweakProviders: [TweakProvider]) { + self.tweakProviders = tweakProviders + for (index, _) in self.tweakProviders.enumerated() { + self.tweakProviders[index].logClosure = logClosure } let notificationCenter = NotificationCenter.default - notificationCenter.addObserver(self, selector: #selector(configurationDidChange), name: TweakConfigurationDidChangeNotification, object: nil) + notificationCenter.addObserver(self, selector: #selector(configurationDidChange), name: TweakProviderDidChangeNotification, object: nil) } deinit { @@ -50,7 +50,7 @@ final public class TweakManager { } } -extension TweakManager: MutableConfiguration { +extension TweakManager: MutableTweakProvider { public func isFeatureEnabled(_ feature: String) -> Bool { queue.sync { @@ -60,7 +60,7 @@ extension TweakManager: MutableConfiguration { } var enabled = false - for (_, configuration) in configurations.enumerated() { + for (_, configuration) in tweakProviders.enumerated() { if configuration.isFeatureEnabled(feature) { enabled = true break @@ -81,7 +81,7 @@ extension TweakManager: MutableConfiguration { } var result: Tweak? = nil - for (_, configuration) in configurations.enumerated() { + for (_, configuration) in tweakProviders.enumerated() { if let tweak = configuration.tweakWith(feature: feature, variable: variable) { logClosure?("Tweak '\(tweak)' found in configuration \(configuration))", .verbose) result = Tweak(feature: feature, @@ -121,7 +121,7 @@ extension TweakManager: MutableConfiguration { } var activeVariation: String? - for (_, configuration) in configurations.enumerated() { + for (_, configuration) in tweakProviders.enumerated() { activeVariation = configuration.activeVariation(for: experiment) if activeVariation != nil { break } } @@ -133,7 +133,7 @@ extension TweakManager: MutableConfiguration { } public func set(_ value: TweakValue, feature: String, variable: String) { - guard let mutableConfiguration = self.mutableConfiguration else { return } + guard let mutableTweakProvider = self.mutableTweakProvider else { return } if useCache { queue.sync { // cannot use write-through cache because tweakWith(feature:variable:) returns a Tweak, but here we only have a TweakValue @@ -141,17 +141,17 @@ extension TweakManager: MutableConfiguration { tweakCache[feature]?[variable] = nil } } - mutableConfiguration.set(value, feature: feature, variable: variable) + mutableTweakProvider.set(value, feature: feature, variable: variable) } public func deleteValue(feature: String, variable: String) { - guard let mutableConfiguration = self.mutableConfiguration else { return } + guard let mutableTweakProvider = self.mutableTweakProvider else { return } if useCache { queue.sync { tweakCache[feature]?[variable] = nil } } - mutableConfiguration.deleteValue(feature: feature, variable: variable) + mutableTweakProvider.deleteValue(feature: feature, variable: variable) } } @@ -161,10 +161,10 @@ extension TweakManager { self.deregisterFromConfigurationsUpdates(object) queue.sync { let queue = OperationQueue.main - let name = TweakConfigurationDidChangeNotification + let name = TweakProviderDidChangeNotification let notificationsCenter = NotificationCenter.default let observer = notificationsCenter.addObserver(forName: name, object: nil, queue: queue) { notification in - guard let tweak = notification.userInfo?[TweakConfigurationDidChangeNotificationTweakKey] as? Tweak else { return } + guard let tweak = notification.userInfo?[TweakProviderDidChangeNotificationTweakKey] as? Tweak else { return } closure(tweak) } observersMap[object] = observer diff --git a/JustTweak/Classes/Configurations/EphemeralConfiguration.swift b/JustTweak/Classes/TweakProviders/EphemeralTweakProvider.swift similarity index 90% rename from JustTweak/Classes/Configurations/EphemeralConfiguration.swift rename to JustTweak/Classes/TweakProviders/EphemeralTweakProvider.swift index 36905fd..267f695 100644 --- a/JustTweak/Classes/Configurations/EphemeralConfiguration.swift +++ b/JustTweak/Classes/TweakProviders/EphemeralTweakProvider.swift @@ -1,11 +1,11 @@ // -// EphemeralConfiguration.swift +// EphemeralTweakProvider.swift // Copyright (c) 2018 Just Eat Holding Ltd. All rights reserved. // import Foundation -extension NSDictionary: Configuration { +extension NSDictionary: TweakProvider { public var logClosure: LogClosure? { get { return nil } @@ -34,7 +34,7 @@ extension NSDictionary: Configuration { } } -extension NSMutableDictionary: MutableConfiguration { +extension NSMutableDictionary: MutableTweakProvider { public func set(_ value: TweakValue, feature: String, variable: String) { self[variable] = value diff --git a/JustTweak/Classes/Configurations/LocalConfiguration.swift b/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift similarity index 95% rename from JustTweak/Classes/Configurations/LocalConfiguration.swift rename to JustTweak/Classes/TweakProviders/LocalTweakProvider.swift index ebc94cf..a635514 100644 --- a/JustTweak/Classes/Configurations/LocalConfiguration.swift +++ b/JustTweak/Classes/TweakProviders/LocalTweakProvider.swift @@ -1,11 +1,11 @@ // -// LocalConfiguration.swift +// LocalTweakProvider.swift // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved. // import Foundation -final public class LocalConfiguration { +final public class LocalTweakProvider { private enum EncodingKeys : String { case Title, Description, Group, Value @@ -53,7 +53,7 @@ final public class LocalConfiguration { } } -extension LocalConfiguration: Configuration { +extension LocalTweakProvider: TweakProvider { public func isFeatureEnabled(_ feature: String) -> Bool { return configurationFile[feature] != nil diff --git a/JustTweak/Classes/Configurations/UserDefaultsConfiguration.swift b/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift similarity index 83% rename from JustTweak/Classes/Configurations/UserDefaultsConfiguration.swift rename to JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift index fe32d67..a8ad1a3 100644 --- a/JustTweak/Classes/Configurations/UserDefaultsConfiguration.swift +++ b/JustTweak/Classes/TweakProviders/UserDefaultsTweakProvider.swift @@ -1,11 +1,11 @@ // -// UserDefaultsConfiguration.swift +// UserDefaultsTweakProvider.swift // Copyright (c) 2016 Just Eat Holding Ltd. All rights reserved. // import Foundation -final public class UserDefaultsConfiguration { +final public class UserDefaultsTweakProvider { private let userDefaults: UserDefaults @@ -18,7 +18,7 @@ final public class UserDefaultsConfiguration { } } -extension UserDefaultsConfiguration: Configuration { +extension UserDefaultsTweakProvider: TweakProvider { public func isFeatureEnabled(_ feature: String) -> Bool { let userDefaultsKey = keyForTweakWithIdentifier(feature) @@ -41,7 +41,7 @@ extension UserDefaultsConfiguration: Configuration { } } -extension UserDefaultsConfiguration: MutableConfiguration { +extension UserDefaultsTweakProvider: MutableTweakProvider { public func set(_ value: TweakValue, feature: String, variable: String) { updateUserDefaults(value: value, feature: feature, variable: variable) @@ -52,10 +52,10 @@ extension UserDefaultsConfiguration: MutableConfiguration { } } -extension UserDefaultsConfiguration { +extension UserDefaultsTweakProvider { private func keyForTweakWithIdentifier(_ identifier: String) -> String { - return "\(UserDefaultsConfiguration.userDefaultsKeyPrefix).\(identifier)" + return "\(UserDefaultsTweakProvider.userDefaultsKeyPrefix).\(identifier)" } private func updateUserDefaults(_ object: AnyObject?) -> TweakValue? { @@ -73,8 +73,8 @@ extension UserDefaultsConfiguration { DispatchQueue.main.async { let notificationCenter = NotificationCenter.default let tweak = Tweak(feature: feature, variable: variable, value: value) - let userInfo = [TweakConfigurationDidChangeNotificationTweakKey: tweak] - notificationCenter.post(name: TweakConfigurationDidChangeNotification, + let userInfo = [TweakProviderDidChangeNotificationTweakKey: tweak] + notificationCenter.post(name: TweakProviderDidChangeNotification, object: self, userInfo: userInfo) } diff --git a/JustTweak/Classes/UI/TweakManager+Presentation.swift b/JustTweak/Classes/UI/TweakManager+Presentation.swift index a812266..b46e48c 100644 --- a/JustTweak/Classes/UI/TweakManager+Presentation.swift +++ b/JustTweak/Classes/UI/TweakManager+Presentation.swift @@ -9,11 +9,11 @@ extension TweakManager { var displayableTweaks: [Tweak] { var tweaks = [String : Tweak]() - for localConfiguration in self.localConfigurations.reversed() { - for (feature, variables) in localConfiguration.features { + for localTweakProvider in self.localTweakProviders.reversed() { + for (feature, variables) in localTweakProvider.features { for variable in variables { if let tweak = tweakWith(feature: feature, variable: variable), - let jsonTweak = localConfiguration.tweakWith(feature: feature, variable: variable) { + let jsonTweak = localTweakProvider.tweakWith(feature: feature, variable: variable) { let aggregatedTweak = Tweak(feature: feature, variable: variable, value: tweak.value, @@ -29,7 +29,7 @@ extension TweakManager { return tweaks.values.sorted(by: { $0.displayTitle < $1.displayTitle }) } - private var localConfigurations: [LocalConfiguration] { - return configurations.filter { $0 is LocalConfiguration } as! [LocalConfiguration] + private var localTweakProviders: [LocalTweakProvider] { + return tweakProviders.filter { $0 is LocalTweakProvider } as! [LocalTweakProvider] } } diff --git a/JustTweak/Classes/Utilities/PropertyWrappers.swift b/JustTweak/Classes/Utilities/PropertyWrappers.swift index 157ac26..919dde8 100644 --- a/JustTweak/Classes/Utilities/PropertyWrappers.swift +++ b/JustTweak/Classes/Utilities/PropertyWrappers.swift @@ -7,6 +7,29 @@ import Foundation @propertyWrapper public struct TweakProperty { + let feature: String + let variable: String + let tweakManager: TweakManager + + public init(feature: String, variable: String, tweakManager: TweakManager) { + self.feature = feature + self.variable = variable + self.tweakManager = tweakManager + } + + public var wrappedValue: T { + get { + let tweak = tweakManager.tweakWith(feature: feature, variable: variable) + return tweak!.value as! T + } + set { + tweakManager.set(newValue, feature: feature, variable: variable) + } + } +} + +@propertyWrapper +public struct FallbackTweakProperty { let fallbackValue: T let feature: String let variable: String diff --git a/README.md b/README.md index 975d9f7..ca6b7db 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ [![License](https://img.shields.io/cocoapods/l/JustTweak.svg?style=flat)](http://cocoapods.org/pods/JustTweak) [![Platform](https://img.shields.io/cocoapods/p/JustTweak.svg?style=flat)](http://cocoapods.org/pods/JustTweak) -JustTweak is a framework for feature flagging and A/B testing for iOS apps. -It provides a simple facade interface interacting with multiple providers that are queried respecting a given priority. +JustTweak is a feature flagging framework for iOS apps. +It provides a simple facade interface interacting with multiple tweak providers that are queried respecting a given priority. Tweaks represent flags used to drive decisions in the client code. With JustTweak you can achieve the following: -- use a JSON local configuration providing default values for experimentation -- use a number of remote configuration providers such as Firebase and Optmizely to run A/B tests and feature flagging +- use a JSON file to provide the default values for feature flagging +- use a number of remote tweak providers such as Firebase and Optmizely to run A/B tests and feature flagging - enable, disable, and customize features locally at runtime - provide a dedicated UI for customization (this comes particularly handy for feature that are under development to showcase it to stakeholders) @@ -31,39 +31,144 @@ pod "JustTweak" ### Integration -- define a JSON configuration file including your features (you can use the included `ExampleConfiguration.json` as a template) -- define your features and A/B tests in your services such as Firebase and Optmizely (optional) -- configure the JustTweak stack as following +- Define a `LocalTweakProvider` JSON file including your features. Refer to `LocalTweaks_example.json` for a starting point. +- Configure the stack + +To configure the stack, you have two options: + +- implement the stack manually +- leverage the code generator tool + +#### Manual integration + +- Configure the JustTweak stack as following: ```swift static let tweakManager: TweakManager = { - // mutable configuration (to override tweaks from other configurations) - let userDefaultsConfiguration = UserDefaultsConfiguration(userDefaults: UserDefaults.standard) - - // remote configurations (optional) - let optimizelyConfiguration = OptimizelyConfiguration() - optimizelyConfiguration.userId = <#user_id#> - let firebaseConfiguration = FirebaseConfiguration() - - // local JSON configuration (default tweaks) - let jsonFileURL = Bundle.main.url(forResource: "ExampleConfiguration", withExtension: "json")! - let localConfiguration = LocalConfiguration(jsonURL: jsonFileURL) - - // priority is defined by the order in the configurations array (from highest to lowest) - let configurations: [Configuration] = [userDefaultsConfiguration, - optimizelyConfiguration, - firebaseConfiguration, - localConfiguration] - return TweakManager(configurations: configurations) + var tweakProviders: [TweakProvider] = [] + + // Mutable TweakProvider (to override tweaks from other TweakProviders) + let userDefaultsTweakProvider = UserDefaultsTweakProvider(userDefaults: UserDefaults.standard) + tweakProviders.append(userDefaultsTweakProvider) + + // Optimizely (remote TweakProvider) + let optimizelyTweakProvider = OptimizelyTweakProvider() + optimizelyTweakProvider.userId = UUID().uuidString + tweakProviders.append(optimizelyTweakProvider) + + // Firebase Remote Config (remote TweakProvider) + let firebaseTweakProvider = FirebaseTweakProvider() + tweakProviders.append(firebaseTweakProvider) + + // Local JSON-based TweakProvider (default TweakProvider) + let jsonFileURL = Bundle.main.url(forResource: "LocalTweaks_example", withExtension: "json")! + let localTweakProvider = LocalTweakProvider(jsonURL: jsonFileURL) + tweakProviders.append(localTweakProvider) + + return TweakManager(tweakProviders: tweakProviders) }() ``` -The order of the objects in the `configurations` array defines the priority of the configurations. +- Implement the properties and constants for your features, backed by the `LocalTweakProvider`. Refer to `TweakAccessor.swift` for a starting point. + +#### Using the code generator tool + +- Define the stack configuration in a `config.json` file in the following format: + +```json +{ + "tweakProviders": [ + { + "type": "EphemeralTweakProvider", + "macros": ["DEBUG", "CONFIGURATION_UI_TESTS"] + }, + { + "type": "UserDefaultsTweakProvider", + "parameter": "UserDefaults.standard", + "macros": ["DEBUG", "CONFIGURATION_DEBUG"] + }, + { + "type": "LocalTweakProvider", + "parameter": "LocalTweaks_TopPriority_example", + "macros": ["DEBUG"] + }, + { + "type": "LocalTweakProvider", + "parameter": "LocalTweaks_example" + } + ], + "shouldCacheTweaks": true, + "accessorName": "GeneratedTweakAccessor" +} +``` + +Supported types are: + +- `EphemeralTweakProvider` +- `UserDefaultsTweakProvider` +- `LocalTweakProvider` +- `CustomTweakProvider` + +The content of the `parameter` value depends on the type: +- `EphemeralTweakProvider`: is not needed +- `UserDefaultsTweakProvider`: should specify the `UserDefaults` instance to be used +- `LocalTweakProvider`: should specify the filename of the json file containing the tweaks +- `CustomTweakProvider`: should specify the filename of a file containing the setup code needed to instantiate and add your custom tweak provider. + +``` +...json +{ + "type": "CustomTweakProvider", + "parameter": "FirebaseTweakProviderSetupCode", + "macros": ["CONFIGURATION_APPSTORE"] +}, +... +``` + +Example content from `FirebaseTweakProviderSetupCode`. Store any CustomTweakProvider setup code file in the same folder of `config.json` and mind not to add them in any target in Xcode. + +```swift +let firebaseTweakProvider = FirebaseTweakProvider() +firebaseTweakProvider.someValue = true +tweakProviders.append(firebaseTweakProvider) +``` + +It's important to include the `tweakProviders.append(<#property_name#>)` statement at the end of your code block that will be included in the generated code. + + +- Add the following to your `Podfile` + +```sh +script_phase :name => 'TweakAccessorGenerator', + :script => '$PODS_ROOT/JustTweak/JustTweak/Assets/TweakAccessorGenerator.bundle/TweakAccessorGenerator \ + -l $SRCROOT/ \ + -o $SRCROOT/ \ + -c $SRCROOT/', + :execution_position => :before_compile +``` + +Every time the target is built, the code generator tool will regenerate the code for the stack. It will include all the properties backing the features defined in the `LocalTweakProvider`. + +- Add the generated files to you project and start using the stack. -The `MutableConfiguration` with the highest priority, such as `UserDefaultsConfiguration` in the example above, will be used to reflect the changes made in the UI (`TweakViewController`). The `LocalConfiguration` should have the lowest priority as it provides the default values from a local configuration and it's the one used by the `TweakViewController` to populate the UI. +## Usage +### Basic -### Usage +If you have used the code generator tool, the generated stack includes all the feature flags. Simply allocate the accessor object (which name you have defined in the `.json` configuration and use it to access the feature flags. + +```swift +let accessor = GeneratedTweakAccessor() +if accessor.meaningOfLife == 42 { + ... +} +``` + +See `GeneratedTweakAccessor.swift` and `GeneratedTweakAccessor+Constants.swift` for an example of generated code. + +### Advanced + +If you decided to implement the stack code yourself, you'll have to implemented code for accessing the features via the `TweakManager`. The three main features of JustTweak can be accessed from the `TweakManager` instance to drive code path decisions. @@ -79,7 +184,7 @@ if enabled { } ``` -2. Get the value of a flag for a given feature. `TweakManager` will return the value from the configuration with the highest priority and automatically fallback to the others if no set value is found. +2. Get the value of a flag for a given feature. `TweakManager` will return the value from the tweak provider with the highest priority and automatically fallback to the others if no set value is found. Use either `tweakWith(feature:variable:)` or the provided property wrappers. @@ -87,23 +192,22 @@ Use either `tweakWith(feature:variable:)` or the provided property wrappers. // check for a tweak value let tweak = tweakManager.tweakWith(feature: "some_feature", variable: "some_flag") if let tweak = tweak { - // tweak was found in some configuration, use tweak.value + // tweak was found in some tweak provider, use tweak.value } else { - // tweak was not found in any configuration + // tweak was not found in any tweak provider } ``` -`@TweakProperty` and `@OptionalTweakProperty` property wrappers are available to mark properties representing feature flags. Mind that by using these property wrappers, a static instance of `TweakManager` is needed. +`@TweakProperty`, `@OptionalTweakProperty` and `@FallbackTweakProperty` property wrappers are available to mark properties representing feature flags. Mind that in order to use these property wrappers, a static instance of `TweakManager` is needed. -``` -@TweakProperty(fallbackValue: <#fallback_value#>, - feature: <#feature_key#>, +```swift +@TweakProperty(feature: <#feature_key#>, variable: <#variable_key#>, tweakManager: <#TweakManager#>) var labelText: String ``` -``` +```swift @OptionalTweakProperty(fallbackValue: <#nillable_fallback_value#>, feature: <#feature_key#>, variable: <#variable_key#>, @@ -111,6 +215,14 @@ var labelText: String var meaningOfLife: Int? ``` +```swift +@FallbackTweakProperty(fallbackValue: <#nillable_fallback_value#>, + feature: <#feature_key#>, + variable: <#variable_key#>, + tweakManager: <#TweakManager#>) +var shouldShowFeatureX: Bool +``` + 3. Run an A/B test ```swift @@ -123,25 +235,37 @@ if let variation = variation { } ``` +See `TweakAccessor.swift` for more info. + + +### Tweak Providers priority + +The order of the objects in the `tweakProviders` array defines the priority of the tweak providers. + +The `MutableTweakProvider` with the highest priority, such as `UserDefaultsTweakProvider` in the example above, will be used to reflect the changes made in the UI (`TweakViewController`). The `LocalTweakProvider` should have the lowest priority as it provides the default values from a local tweak provider and it's the one used by the `TweakViewController` to populate the UI. + +### Migration notes + +In order to migrate from manual to the code generated implementation, it is necessary to update to the new `.json` format. To aid with this process we have added the `GeneratedPropertyName` property to the tweak object. Set this value to align with your current property names in code, so that the generated accessor properties match your existing implementation. -### Caching +### Caching notes The `TweakManager` provides the option to cache the tweak values in order to improve performance. Caching is disabled by default but can be enabled via the `useCache` property. When enabled, there are two ways to reset the cache: - call the `resetCache` method on the `TweakManager` -- post a `TweakConfigurationDidChangeNotification` notification +- post a `TweakProviderDidChangeNotification` notification -### Update a configuration at runtime +### Update a mutable tweak provider at runtime -JustTweak comes with a ViewController that allows the user to edit the `MutableConfiguration` with the highest priority. +JustTweak comes with a ViewController that allows the user to edit the `MutableTweakProvider` with the highest priority. ```swift func presentTweakViewController() { let tweakViewController = TweakViewController(style: .grouped, tweakManager: <#TweakManager#>) // either present it modally - let tweaksNavigationController = UINavigationController(rootViewController:tweakViewController) + let tweaksNavigationController = UINavigationController(rootViewController:tweakViewController) tweaksNavigationController.navigationBar.prefersLargeTitles = true present(tweaksNavigationController, animated: true, completion: nil) @@ -150,14 +274,14 @@ func presentTweakViewController() { } ``` -When a value is modified in any `MutableConfiguration`, a notification is fired to give the clients the opportunity to react and reflect changes in the UI. +When a value is modified in any `MutableTweakProvider`, a notification is fired to give the clients the opportunity to react and reflect changes in the UI. ```swift override func viewDidLoad() { super.viewDidLoad() NotificationCenter.defaultCenter().addObserver(self, selector: #selector(updateUI), - name: TweakConfigurationDidChangeNotification, + name: TweakProviderDidChangeNotification, object: nil) } @@ -169,13 +293,13 @@ override func viewDidLoad() { ### Customization -JustTweak comes with three configurations out-of-the-box: +JustTweak comes with three tweak providers out-of-the-box: -- `UserDefaultsConfiguration` which is mutable and uses `UserDefaults` as a key/value store -- `LocalConfiguration` which is read-only and uses a JSON configuration file that is meant to be the default configuration -- `EphemeralConfiguration` which is simply an instance of `NSMutableDictionary` +- `UserDefaultsTweakProvider` which is mutable and uses `UserDefaults` as a key/value store +- `LocalTweakProvider` which is read-only and uses a JSON file that is meant to hold the default feature flagging setup +- `EphemeralTweakProvider` which is simply an instance of `NSMutableDictionary` -In addition, JustTweak defines `Configuration` and `MutableConfiguration` protocols you can implement to create your own configurations to fit your needs. In the example project you can find a few example configurations which you can use as a starting point. +In addition, JustTweak defines `TweakProvider` and `MutableTweakProvider` protocols you can implement to create your own tweak provider to fit your needs. In the example project you can find some examples which you can use as a starting point. ## License diff --git a/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.pbxproj b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.pbxproj new file mode 100644 index 0000000..fafdd95 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.pbxproj @@ -0,0 +1,591 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 120B0A1E26285CE70066F2C2 /* GeneratedTweakAccessor+ConstantsContent in Resources */ = {isa = PBXBuildFile; fileRef = 120B0A1D26285CE70066F2C2 /* GeneratedTweakAccessor+ConstantsContent */; }; + 12273D1B2625D6BE00732559 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12273D1A2625D6BE00732559 /* main.swift */; }; + 12273D382625E45F00732559 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 12273D372625E45F00732559 /* ArgumentParser */; }; + 12BE964D2626FA5000C1B6C3 /* TweakAccessorCodeGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12BE964C2626FA5000C1B6C3 /* TweakAccessorCodeGeneratorTests.swift */; }; + 12BE987A2627243D00C1B6C3 /* Tweaks.json in Resources */ = {isa = PBXBuildFile; fileRef = 12BE98792627243D00C1B6C3 /* Tweaks.json */; }; + 12BE989D26272D6200C1B6C3 /* GeneratedTweakAccessorContent in Resources */ = {isa = PBXBuildFile; fileRef = 12BE989C26272D6200C1B6C3 /* GeneratedTweakAccessorContent */; }; + 4F5B53E22632DD6100513C18 /* OptimizelyTweakProviderSetupCode in Resources */ = {isa = PBXBuildFile; fileRef = 4F5B53E02632DD6100513C18 /* OptimizelyTweakProviderSetupCode */; }; + 4F5B53E32632DD6100513C18 /* FirebaseTweakProviderSetupCode in Resources */ = {isa = PBXBuildFile; fileRef = 4F5B53E12632DD6100513C18 /* FirebaseTweakProviderSetupCode */; }; + 4F65E88B2629CF8C009E8C3B /* ValidTweaks_TopPriority.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F65E8892629CF8C009E8C3B /* ValidTweaks_TopPriority.json */; }; + 4F65E88C2629CF8C009E8C3B /* ValidTweaks_LowPriority.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F65E88A2629CF8C009E8C3B /* ValidTweaks_LowPriority.json */; }; + 4F7750762627338700A0A3F5 /* TweakValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750732627338700A0A3F5 /* TweakValue.swift */; }; + 4F7750772627338700A0A3F5 /* TweakValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750732627338700A0A3F5 /* TweakValue.swift */; }; + 4F7750782627338700A0A3F5 /* TweakLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750742627338700A0A3F5 /* TweakLoader.swift */; }; + 4F7750792627338700A0A3F5 /* TweakLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750742627338700A0A3F5 /* TweakLoader.swift */; }; + 4F77507A2627338700A0A3F5 /* TweakAccessorCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750752627338700A0A3F5 /* TweakAccessorCodeGenerator.swift */; }; + 4F77507B2627338700A0A3F5 /* TweakAccessorCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750752627338700A0A3F5 /* TweakAccessorCodeGenerator.swift */; }; + 4F7750862627338E00A0A3F5 /* NSNumber+ValueTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750812627338E00A0A3F5 /* NSNumber+ValueTypes.swift */; }; + 4F7750872627338E00A0A3F5 /* NSNumber+ValueTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750812627338E00A0A3F5 /* NSNumber+ValueTypes.swift */; }; + 4F7750882627338E00A0A3F5 /* String+Casing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750822627338E00A0A3F5 /* String+Casing.swift */; }; + 4F7750892627338E00A0A3F5 /* String+Casing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750822627338E00A0A3F5 /* String+Casing.swift */; }; + 4F775097262751EE00A0A3F5 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F775096262751EE00A0A3F5 /* Models.swift */; }; + 4F775098262751EE00A0A3F5 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F775096262751EE00A0A3F5 /* Models.swift */; }; + 4F77509C262751FD00A0A3F5 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F77509B262751FD00A0A3F5 /* Types.swift */; }; + 4F77509D262751FD00A0A3F5 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F77509B262751FD00A0A3F5 /* Types.swift */; }; + 4F7750AA2627603A00A0A3F5 /* NSNumber+ValueTypesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750A92627603A00A0A3F5 /* NSNumber+ValueTypesTests.swift */; }; + 4F7750B32627626100A0A3F5 /* String+CasingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750B22627626100A0A3F5 /* String+CasingTests.swift */; }; + 4F7750B9262764C600A0A3F5 /* TweakLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7750B8262764C600A0A3F5 /* TweakLoaderTests.swift */; }; + 4F7750C126277B6300A0A3F5 /* InvalidTweaks_MissingValues.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F7750C026277B6300A0A3F5 /* InvalidTweaks_MissingValues.json */; }; + 4FD526F826283EBB000F6780 /* InvalidTweaks_DuplicateGeneratedPropertyName.json in Resources */ = {isa = PBXBuildFile; fileRef = 4FD526F726283EBB000F6780 /* InvalidTweaks_DuplicateGeneratedPropertyName.json */; }; + 4FD526FE26283F91000F6780 /* InvalidTweaks_InvalidJSON.json in Resources */ = {isa = PBXBuildFile; fileRef = 4FD526FD26283F91000F6780 /* InvalidTweaks_InvalidJSON.json */; }; + 4FD52706262841C7000F6780 /* Array+Duplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD52705262841C7000F6780 /* Array+Duplicates.swift */; }; + 4FD5270C262842B3000F6780 /* Array+DuplicatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD5270B262842B3000F6780 /* Array+DuplicatesTests.swift */; }; + 4FD52711262843B6000F6780 /* Array+Duplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD52705262841C7000F6780 /* Array+Duplicates.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 12273D152625D6BE00732559 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 120B0A1D26285CE70066F2C2 /* GeneratedTweakAccessor+ConstantsContent */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "GeneratedTweakAccessor+ConstantsContent"; sourceTree = ""; }; + 12273D172625D6BE00732559 /* TweakAccessorGenerator */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = TweakAccessorGenerator; sourceTree = BUILT_PRODUCTS_DIR; }; + 12273D1A2625D6BE00732559 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 12BE964A2626FA5000C1B6C3 /* TweakAccessorGeneratorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TweakAccessorGeneratorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 12BE964C2626FA5000C1B6C3 /* TweakAccessorCodeGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweakAccessorCodeGeneratorTests.swift; sourceTree = ""; }; + 12BE964E2626FA5000C1B6C3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 12BE98792627243D00C1B6C3 /* Tweaks.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Tweaks.json; sourceTree = ""; }; + 12BE989C26272D6200C1B6C3 /* GeneratedTweakAccessorContent */ = {isa = PBXFileReference; lastKnownFileType = text; path = GeneratedTweakAccessorContent; sourceTree = ""; }; + 4F5B53E02632DD6100513C18 /* OptimizelyTweakProviderSetupCode */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = OptimizelyTweakProviderSetupCode; sourceTree = ""; }; + 4F5B53E12632DD6100513C18 /* FirebaseTweakProviderSetupCode */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = FirebaseTweakProviderSetupCode; sourceTree = ""; }; + 4F5B54AF263413BE00513C18 /* OptimizelyTweakProviderSetupCode */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = OptimizelyTweakProviderSetupCode; sourceTree = ""; }; + 4F5B54B0263413BE00513C18 /* config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = config.json; sourceTree = ""; }; + 4F5B54B1263413BE00513C18 /* FirebaseTweakProviderSetupCode */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = FirebaseTweakProviderSetupCode; sourceTree = ""; }; + 4F5B54B3263413BE00513C18 /* config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = config.json; sourceTree = ""; }; + 4F65E8892629CF8C009E8C3B /* ValidTweaks_TopPriority.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ValidTweaks_TopPriority.json; sourceTree = ""; }; + 4F65E88A2629CF8C009E8C3B /* ValidTweaks_LowPriority.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ValidTweaks_LowPriority.json; sourceTree = ""; }; + 4F7750732627338700A0A3F5 /* TweakValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweakValue.swift; sourceTree = ""; }; + 4F7750742627338700A0A3F5 /* TweakLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweakLoader.swift; sourceTree = ""; }; + 4F7750752627338700A0A3F5 /* TweakAccessorCodeGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweakAccessorCodeGenerator.swift; sourceTree = ""; }; + 4F7750812627338E00A0A3F5 /* NSNumber+ValueTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSNumber+ValueTypes.swift"; sourceTree = ""; }; + 4F7750822627338E00A0A3F5 /* String+Casing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Casing.swift"; sourceTree = ""; }; + 4F775096262751EE00A0A3F5 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 4F77509B262751FD00A0A3F5 /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; + 4F7750A92627603A00A0A3F5 /* NSNumber+ValueTypesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSNumber+ValueTypesTests.swift"; sourceTree = ""; }; + 4F7750B22627626100A0A3F5 /* String+CasingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+CasingTests.swift"; sourceTree = ""; }; + 4F7750B8262764C600A0A3F5 /* TweakLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweakLoaderTests.swift; sourceTree = ""; }; + 4F7750C026277B6300A0A3F5 /* InvalidTweaks_MissingValues.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidTweaks_MissingValues.json; sourceTree = ""; }; + 4FD526F726283EBB000F6780 /* InvalidTweaks_DuplicateGeneratedPropertyName.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidTweaks_DuplicateGeneratedPropertyName.json; sourceTree = ""; }; + 4FD526FD26283F91000F6780 /* InvalidTweaks_InvalidJSON.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidTweaks_InvalidJSON.json; sourceTree = ""; }; + 4FD52705262841C7000F6780 /* Array+Duplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Duplicates.swift"; sourceTree = ""; }; + 4FD5270B262842B3000F6780 /* Array+DuplicatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+DuplicatesTests.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 12273D142625D6BE00732559 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 12273D382625E45F00732559 /* ArgumentParser in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 12BE96472626FA5000C1B6C3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 12273D0E2625D6BE00732559 = { + isa = PBXGroup; + children = ( + 12273D192625D6BE00732559 /* TweakAccessorGenerator */, + 12BE964B2626FA5000C1B6C3 /* TweakAccessorGeneratorTests */, + 12273D182625D6BE00732559 /* Products */, + ); + sourceTree = ""; + }; + 12273D182625D6BE00732559 /* Products */ = { + isa = PBXGroup; + children = ( + 12273D172625D6BE00732559 /* TweakAccessorGenerator */, + 12BE964A2626FA5000C1B6C3 /* TweakAccessorGeneratorTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 12273D192625D6BE00732559 /* TweakAccessorGenerator */ = { + isa = PBXGroup; + children = ( + 4FD5296A26288707000F6780 /* Assets */, + 4F775035262718DE00A0A3F5 /* Core */, + 4F775034262718D600A0A3F5 /* Extensions */, + 12273D1A2625D6BE00732559 /* main.swift */, + ); + path = TweakAccessorGenerator; + sourceTree = ""; + }; + 12BE964B2626FA5000C1B6C3 /* TweakAccessorGeneratorTests */ = { + isa = PBXGroup; + children = ( + 4F7750AF2627603E00A0A3F5 /* Suites */, + 4F7750A82627602200A0A3F5 /* Assets */, + 12BE964E2626FA5000C1B6C3 /* Info.plist */, + ); + path = TweakAccessorGeneratorTests; + sourceTree = ""; + }; + 4F5B54AE263413BE00513C18 /* TestConfiguration2 */ = { + isa = PBXGroup; + children = ( + 4F5B54AF263413BE00513C18 /* OptimizelyTweakProviderSetupCode */, + 4F5B54B0263413BE00513C18 /* config.json */, + 4F5B54B1263413BE00513C18 /* FirebaseTweakProviderSetupCode */, + ); + path = TestConfiguration2; + sourceTree = ""; + }; + 4F5B54B2263413BE00513C18 /* TestConfiguration1 */ = { + isa = PBXGroup; + children = ( + 4F5B54B3263413BE00513C18 /* config.json */, + ); + path = TestConfiguration1; + sourceTree = ""; + }; + 4F775034262718D600A0A3F5 /* Extensions */ = { + isa = PBXGroup; + children = ( + 4FD52705262841C7000F6780 /* Array+Duplicates.swift */, + 4F7750812627338E00A0A3F5 /* NSNumber+ValueTypes.swift */, + 4F7750822627338E00A0A3F5 /* String+Casing.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 4F775035262718DE00A0A3F5 /* Core */ = { + isa = PBXGroup; + children = ( + 4F7750752627338700A0A3F5 /* TweakAccessorCodeGenerator.swift */, + 4F7750742627338700A0A3F5 /* TweakLoader.swift */, + 4F775096262751EE00A0A3F5 /* Models.swift */, + 4F77509B262751FD00A0A3F5 /* Types.swift */, + 4F7750732627338700A0A3F5 /* TweakValue.swift */, + ); + path = Core; + sourceTree = ""; + }; + 4F7750A82627602200A0A3F5 /* Assets */ = { + isa = PBXGroup; + children = ( + 4F5B53E12632DD6100513C18 /* FirebaseTweakProviderSetupCode */, + 4F5B53E02632DD6100513C18 /* OptimizelyTweakProviderSetupCode */, + 4FD526F726283EBB000F6780 /* InvalidTweaks_DuplicateGeneratedPropertyName.json */, + 4FD526FD26283F91000F6780 /* InvalidTweaks_InvalidJSON.json */, + 4F7750C026277B6300A0A3F5 /* InvalidTweaks_MissingValues.json */, + 12BE98792627243D00C1B6C3 /* Tweaks.json */, + 4F65E88A2629CF8C009E8C3B /* ValidTweaks_LowPriority.json */, + 4F65E8892629CF8C009E8C3B /* ValidTweaks_TopPriority.json */, + 12BE989C26272D6200C1B6C3 /* GeneratedTweakAccessorContent */, + 120B0A1D26285CE70066F2C2 /* GeneratedTweakAccessor+ConstantsContent */, + ); + path = Assets; + sourceTree = ""; + }; + 4F7750AF2627603E00A0A3F5 /* Suites */ = { + isa = PBXGroup; + children = ( + 4FD5270B262842B3000F6780 /* Array+DuplicatesTests.swift */, + 4F7750B8262764C600A0A3F5 /* TweakLoaderTests.swift */, + 4F7750A92627603A00A0A3F5 /* NSNumber+ValueTypesTests.swift */, + 4F7750B22627626100A0A3F5 /* String+CasingTests.swift */, + 12BE964C2626FA5000C1B6C3 /* TweakAccessorCodeGeneratorTests.swift */, + ); + path = Suites; + sourceTree = ""; + }; + 4FD5296A26288707000F6780 /* Assets */ = { + isa = PBXGroup; + children = ( + 4F5B54B2263413BE00513C18 /* TestConfiguration1 */, + 4F5B54AE263413BE00513C18 /* TestConfiguration2 */, + ); + path = Assets; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 12273D162625D6BE00732559 /* TweakAccessorGenerator */ = { + isa = PBXNativeTarget; + buildConfigurationList = 12273D1E2625D6BE00732559 /* Build configuration list for PBXNativeTarget "TweakAccessorGenerator" */; + buildPhases = ( + 12273D132625D6BE00732559 /* Sources */, + 12273D142625D6BE00732559 /* Frameworks */, + 12273D152625D6BE00732559 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TweakAccessorGenerator; + packageProductDependencies = ( + 12273D372625E45F00732559 /* ArgumentParser */, + ); + productName = TweakAccessorGenerator; + productReference = 12273D172625D6BE00732559 /* TweakAccessorGenerator */; + productType = "com.apple.product-type.tool"; + }; + 12BE96492626FA5000C1B6C3 /* TweakAccessorGeneratorTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 12BE96512626FA5000C1B6C3 /* Build configuration list for PBXNativeTarget "TweakAccessorGeneratorTests" */; + buildPhases = ( + 12BE96462626FA5000C1B6C3 /* Sources */, + 12BE96472626FA5000C1B6C3 /* Frameworks */, + 12BE96482626FA5000C1B6C3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TweakAccessorGeneratorTests; + productName = TweakAccessorGeneratorTests; + productReference = 12BE964A2626FA5000C1B6C3 /* TweakAccessorGeneratorTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 12273D0F2625D6BE00732559 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1240; + LastUpgradeCheck = 1240; + TargetAttributes = { + 12273D162625D6BE00732559 = { + CreatedOnToolsVersion = 12.4; + }; + 12BE96492626FA5000C1B6C3 = { + CreatedOnToolsVersion = 12.4; + }; + }; + }; + buildConfigurationList = 12273D122625D6BE00732559 /* Build configuration list for PBXProject "TweakAccessorGenerator" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 12273D0E2625D6BE00732559; + packageReferences = ( + 12273D362625E45F00732559 /* XCRemoteSwiftPackageReference "swift-argument-parser" */, + ); + productRefGroup = 12273D182625D6BE00732559 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 12273D162625D6BE00732559 /* TweakAccessorGenerator */, + 12BE96492626FA5000C1B6C3 /* TweakAccessorGeneratorTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 12BE96482626FA5000C1B6C3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F5B53E32632DD6100513C18 /* FirebaseTweakProviderSetupCode in Resources */, + 120B0A1E26285CE70066F2C2 /* GeneratedTweakAccessor+ConstantsContent in Resources */, + 4FD526FE26283F91000F6780 /* InvalidTweaks_InvalidJSON.json in Resources */, + 4FD526F826283EBB000F6780 /* InvalidTweaks_DuplicateGeneratedPropertyName.json in Resources */, + 4F7750C126277B6300A0A3F5 /* InvalidTweaks_MissingValues.json in Resources */, + 12BE989D26272D6200C1B6C3 /* GeneratedTweakAccessorContent in Resources */, + 4F5B53E22632DD6100513C18 /* OptimizelyTweakProviderSetupCode in Resources */, + 12BE987A2627243D00C1B6C3 /* Tweaks.json in Resources */, + 4F65E88C2629CF8C009E8C3B /* ValidTweaks_LowPriority.json in Resources */, + 4F65E88B2629CF8C009E8C3B /* ValidTweaks_TopPriority.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 12273D132625D6BE00732559 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F7750762627338700A0A3F5 /* TweakValue.swift in Sources */, + 4F7750782627338700A0A3F5 /* TweakLoader.swift in Sources */, + 4F77509C262751FD00A0A3F5 /* Types.swift in Sources */, + 12273D1B2625D6BE00732559 /* main.swift in Sources */, + 4FD52706262841C7000F6780 /* Array+Duplicates.swift in Sources */, + 4F77507A2627338700A0A3F5 /* TweakAccessorCodeGenerator.swift in Sources */, + 4F775097262751EE00A0A3F5 /* Models.swift in Sources */, + 4F7750882627338E00A0A3F5 /* String+Casing.swift in Sources */, + 4F7750862627338E00A0A3F5 /* NSNumber+ValueTypes.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 12BE96462626FA5000C1B6C3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4FD52711262843B6000F6780 /* Array+Duplicates.swift in Sources */, + 4F7750772627338700A0A3F5 /* TweakValue.swift in Sources */, + 4FD5270C262842B3000F6780 /* Array+DuplicatesTests.swift in Sources */, + 4F7750792627338700A0A3F5 /* TweakLoader.swift in Sources */, + 4F77509D262751FD00A0A3F5 /* Types.swift in Sources */, + 4F7750B32627626100A0A3F5 /* String+CasingTests.swift in Sources */, + 4F7750AA2627603A00A0A3F5 /* NSNumber+ValueTypesTests.swift in Sources */, + 12BE964D2626FA5000C1B6C3 /* TweakAccessorCodeGeneratorTests.swift in Sources */, + 4F77507B2627338700A0A3F5 /* TweakAccessorCodeGenerator.swift in Sources */, + 4F775098262751EE00A0A3F5 /* Models.swift in Sources */, + 4F7750892627338E00A0A3F5 /* String+Casing.swift in Sources */, + 4F7750872627338E00A0A3F5 /* NSNumber+ValueTypes.swift in Sources */, + 4F7750B9262764C600A0A3F5 /* TweakLoaderTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 12273D1C2625D6BE00732559 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 12273D1D2625D6BE00732559 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 12273D1F2625D6BE00732559 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ZKPN288GRK; + ENABLE_HARDENED_RUNTIME = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 12273D202625D6BE00732559 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ZKPN288GRK; + ENABLE_HARDENED_RUNTIME = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 12BE964F2626FA5000C1B6C3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ZKPN288GRK; + INFOPLIST_FILE = TweakAccessorGeneratorTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.justeattakeaway.TweakAccessorGeneratorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 12BE96502626FA5000C1B6C3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ZKPN288GRK; + INFOPLIST_FILE = TweakAccessorGeneratorTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.justeattakeaway.TweakAccessorGeneratorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 12273D122625D6BE00732559 /* Build configuration list for PBXProject "TweakAccessorGenerator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 12273D1C2625D6BE00732559 /* Debug */, + 12273D1D2625D6BE00732559 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 12273D1E2625D6BE00732559 /* Build configuration list for PBXNativeTarget "TweakAccessorGenerator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 12273D1F2625D6BE00732559 /* Debug */, + 12273D202625D6BE00732559 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 12BE96512626FA5000C1B6C3 /* Build configuration list for PBXNativeTarget "TweakAccessorGeneratorTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 12BE964F2626FA5000C1B6C3 /* Debug */, + 12BE96502626FA5000C1B6C3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 12273D362625E45F00732559 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "git@github.com:apple/swift-argument-parser.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.4.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 12273D372625E45F00732559 /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = 12273D362625E45F00732559 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 12273D0F2625D6BE00732559 /* Project object */; +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/xcshareddata/xcschemes/TweakAccessorGenerator-Release.xcscheme b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/xcshareddata/xcschemes/TweakAccessorGenerator-Release.xcscheme new file mode 100644 index 0000000..4d5d8c7 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/xcshareddata/xcschemes/TweakAccessorGenerator-Release.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/xcshareddata/xcschemes/TweakAccessorGenerator.xcscheme b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/xcshareddata/xcschemes/TweakAccessorGenerator.xcscheme new file mode 100644 index 0000000..7335392 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator.xcodeproj/xcshareddata/xcschemes/TweakAccessorGenerator.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration1/config.json b/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration1/config.json new file mode 100644 index 0000000..17953e9 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration1/config.json @@ -0,0 +1,24 @@ +{ + "tweakProviders": [ + { + "type": "EphemeralTweakProvider", + "macros": ["DEBUG", "CONFIGURATION_UI_TESTS"] + }, + { + "type": "UserDefaultsTweakProvider", + "parameter": "UserDefaults.standard", + "macros": ["DEBUG", "CONFIGURATION_DEBUG"] + }, + { + "type": "LocalTweakProvider", + "parameter": "LocalTweaks_TopPriority_example", + "macros": ["DEBUG"] + }, + { + "type": "LocalTweakProvider", + "parameter": "LocalTweaks_example" + } + ], + "shouldCacheTweaks": true, + "accessorName": "GeneratedTweakAccessor" +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/FirebaseTweakProviderSetupCode b/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/FirebaseTweakProviderSetupCode new file mode 100644 index 0000000..19b150c --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/FirebaseTweakProviderSetupCode @@ -0,0 +1,3 @@ +let fc = FirebaseTweakProvider() +fc.someValue = true +tweakProviders.append(fc) diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/OptimizelyTweakProviderSetupCode b/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/OptimizelyTweakProviderSetupCode new file mode 100644 index 0000000..209ec24 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/OptimizelyTweakProviderSetupCode @@ -0,0 +1,3 @@ +let optimizelyTweakProvider = OptimizelyTweakProvider() +optimizelyTweakProvider.someValue = 42 +tweakProviders.append(optimizelyTweakProvider) diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/config.json b/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/config.json new file mode 100644 index 0000000..f47055d --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Assets/TestConfiguration2/config.json @@ -0,0 +1,34 @@ +{ + "tweakProviders": [ + { + "type": "EphemeralTweakProvider", + "macros": ["DEBUG", "CONFIGURATION_UI_TESTS"] + }, + { + "type": "UserDefaultsTweakProvider", + "parameter": "UserDefaults.standard", + "macros": ["DEBUG", "CONFIGURATION_DEBUG"] + }, + { + "type": "CustomTweakProvider", + "parameter": "OptimizelyTweakProviderSetupCode", + "macros": ["CONFIGURATION_APPSTORE"] + }, + { + "type": "CustomTweakProvider", + "parameter": "FirebaseTweakProviderSetupCode", + "macros": ["CONFIGURATION_APPSTORE"] + }, + { + "type": "LocalTweakProvider", + "parameter": "LocalTweaks_TopPriority_example", + "macros": ["DEBUG"] + }, + { + "type": "LocalTweakProvider", + "parameter": "LocalTweaks_example" + } + ], + "shouldCacheTweaks": true, + "accessorName": "GeneratedTweakAccessor" +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Core/Models.swift b/TweakAccessorGenerator/TweakAccessorGenerator/Core/Models.swift new file mode 100644 index 0000000..bcb4890 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Core/Models.swift @@ -0,0 +1,28 @@ +// +// Models.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +struct Tweak: Equatable { + let feature: String + let variable: String + let title: String + let description: String? + let group: String + let valueType: String + let propertyName: String? +} + +struct Configuration: Decodable { + let tweakProviders: [TweakProvider] + let shouldCacheTweaks: Bool + let accessorName: String +} + +struct TweakProvider: Decodable { + let type: String + let parameter: String? + let macros: [String]? +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakAccessorCodeGenerator.swift b/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakAccessorCodeGenerator.swift new file mode 100644 index 0000000..16386e6 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakAccessorCodeGenerator.swift @@ -0,0 +1,253 @@ +// +// TweakAccessorCodeGenerator.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +class TweakAccessorCodeGenerator { + + private let featuresConst = "Features" + private let variablesConst = "Variables" + + private let featureConstantsConst = "" + private let variableConstantsConst = "" + private let classContentConst = "" + private let tweakManagerConst = "" +} + +extension TweakAccessorCodeGenerator { + + func generateConstantsFileContent(tweaks: [Tweak], + configuration: Configuration) -> String { + let template = self.constantsTemplate(with: configuration.accessorName) + let featureConstants = self.featureConstantsCodeBlock(with: tweaks) + let variableConstants = self.variableConstantsCodeBlock(with: tweaks) + + let content = template + .replacingOccurrences(of: featureConstantsConst, with: featureConstants) + .replacingOccurrences(of: variableConstantsConst, with: variableConstants) + .trimmingCharacters(in: .whitespacesAndNewlines) + return content + } + + func generateAccessorFileContent(tweaksFilename: String, + tweaks: [Tweak], + configuration: Configuration, + customTweakProvidersSetupCode: [Filename: CodeBlock]) -> String { + let template = self.accessorTemplate(with: configuration.accessorName) + let tweakManager = self.tweakManagerCodeBlock(with: configuration, customTweakProvidersSetupCode: customTweakProvidersSetupCode) + let classContent = self.classContent(with: tweaks) + + let content = template + .replacingOccurrences(of: tweakManagerConst, with: tweakManager) + .replacingOccurrences(of: classContentConst, with: classContent) + .trimmingCharacters(in: .whitespacesAndNewlines) + return content + } + + func constantsTemplate(with className: String) -> String { + """ + // + // \(className)+Constants.swift + // Generated by TweakAccessorGenerator + // + + import Foundation + + extension \(className) { + + \(featureConstantsConst) + + \(variableConstantsConst) + } + """ + } + + private func accessorTemplate(with className: String) -> String { + """ + // + // \(className).swift + // Generated by TweakAccessorGenerator + // + + import Foundation + import JustTweak + + class \(className) { + + \(tweakManagerConst) + + \(classContentConst) + } + """ + } + + private func featureConstantsCodeBlock(with tweaks: [Tweak]) -> String { + var features = Set() + for tweak in tweaks { + features.insert(tweak.feature) + } + let content: [String] = features.map { + """ + static let \($0.camelCased()) = "\($0)" + """ + } + return """ + struct \(featuresConst) { + \(content.sorted().joined(separator: "\n")) + } + """ + } + + private func variableConstantsCodeBlock(with tweaks: [Tweak]) -> String { + var variables = Set() + for tweak in tweaks { + variables.insert(tweak.variable) + } + let content: [String] = variables.map { + """ + static let \($0.camelCased()) = "\($0)" + """ + } + return """ + struct \(variablesConst) { + \(content.sorted().joined(separator: "\n")) + } + """ + } + + private func tweakManagerCodeBlock(with configuration: Configuration, + customTweakProvidersSetupCode: [Filename: CodeBlock]) -> String { + let tweakProvidersCodeBlock = self.tweakProvidersCodeBlock(with: configuration, + customTweakProvidersSetupCode: customTweakProvidersSetupCode) + + return """ + static let tweakManager: TweakManager = { + \(tweakProvidersCodeBlock) + let tweakManager = TweakManager(tweakProviders: tweakProviders) + tweakManager.useCache = \(configuration.shouldCacheTweaks) + return tweakManager + }() + + var tweakManager: TweakManager { + return Self.tweakManager + } + """ + } + + private func tweakProvidersCodeBlock(with configuration: Configuration, + customTweakProvidersSetupCode: [Filename: CodeBlock]) -> String { + let grouping = Dictionary(grouping: configuration.tweakProviders) { $0.type } + + var tweakProvidersString: [String] = [ + """ + var tweakProviders: [TweakProvider] = []\n + """ + ] + + var currentIndexByConf: [String: Int] = grouping.mapValues{ _ in 0 } + + for tweakProvider in configuration.tweakProviders { + let value = grouping[tweakProvider.type]! + let index = currentIndexByConf[tweakProvider.type]! + let tweakProvider = value[index] + let tweakProviderName = "\(tweakProvider.type.lowercasedFirstChar())_\(index+1)" + var generatedString: [String] = [] + let macros = tweakProvider.macros?.joined(separator: " || ") + + let jsonFileURL = "jsonFileURL_\(index+1)" + let headerComment = """ + // \(tweakProvider.type) + """ + generatedString.append(headerComment) + + if macros != nil { + let macroStarting = """ + #if \(macros!) + """ + generatedString.append(macroStarting) + } + + switch tweakProvider.type { + case "EphemeralTweakProvider": + let tweakProviderAllocation = + """ + let \(tweakProviderName) = NSMutableDictionary() + tweakProviders.append(\(tweakProviderName)) + """ + generatedString.append(tweakProviderAllocation) + + case "UserDefaultsTweakProvider": + assert(tweakProvider.parameter != nil, "Missing value 'parameter' for TweakProvider '\(tweakProvider)'") + let tweakProviderAllocation = + """ + let \(tweakProviderName) = \(tweakProvider.type)(userDefaults: \(tweakProvider.parameter!)) + tweakProviders.append(\(tweakProviderName)) + """ + generatedString.append(tweakProviderAllocation) + + case "LocalTweakProvider": + assert(tweakProvider.parameter != nil, "Missing value 'parameter' for TweakProvider '\(tweakProvider)'") + let tweakProviderAllocation = + """ + let \(jsonFileURL) = Bundle.main.url(forResource: \"\(tweakProvider.parameter!)\", withExtension: "json")! + let \(tweakProviderName) = \(tweakProvider.type)(jsonURL: \(jsonFileURL)) + tweakProviders.append(\(tweakProviderName)) + """ + generatedString.append(tweakProviderAllocation) + + case "CustomTweakProvider": + assert(tweakProvider.parameter != nil, "Missing value 'parameter' for TweakProvider '\(tweakProvider)'") + let setupCode = customTweakProvidersSetupCode[tweakProvider.parameter!]! + let formattedSetupCode = formatCustomTweakProviderSetupCode(setupCode) + let tweakProviderAllocation = + """ + \(formattedSetupCode.trimmingCharacters(in: .newlines)) + """ + generatedString.append(tweakProviderAllocation) + + default: + assertionFailure("Unsupported TweakProvider \(tweakProvider)") + break + } + + if macros != nil { + let macroClosing = """ + #endif + """ + generatedString.append(macroClosing) + } + generatedString.append("") + + tweakProvidersString.append(contentsOf: generatedString) + currentIndexByConf[tweakProvider.type] = currentIndexByConf[tweakProvider.type]! + 1 + } + + return tweakProvidersString.joined(separator: "\n") + } + + private func classContent(with tweaks: [Tweak]) -> String { + var content: Set = [] + tweaks.forEach { + content.insert(tweakProperty(for: $0)) + } + return content.sorted().joined(separator: "\n\n") + } + + private func tweakProperty(for tweak: Tweak) -> String { + let propertyName = tweak.propertyName ?? tweak.variable.camelCased() + return """ + @TweakProperty(feature: \(featuresConst).\(tweak.feature.camelCased()), + variable: \(variablesConst).\(tweak.variable.camelCased()), + tweakManager: tweakManager) + var \(propertyName): \(tweak.valueType) + """ + } + + private func formatCustomTweakProviderSetupCode(_ setupCode: String) -> String { + setupCode.split(separator: "\n") + .map { " " + $0 } + .joined(separator: "\n") + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakLoader.swift b/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakLoader.swift new file mode 100644 index 0000000..0336e80 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakLoader.swift @@ -0,0 +1,70 @@ +// +// TweakLoader.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +extension String: Error {} + +class TweakLoader { + + func load(_ filePath: String) throws -> [Tweak] { + let url = URL(fileURLWithPath: filePath) + let data = try Data(contentsOf: url) + guard let content = try JSONSerialization.jsonObject(with: data) as? TweaksFormat else { + throw "Invalid JSON format for file \(filePath)" + } + + let tweaks = try content.map { (featureKey: String, tweaks: [String: [String: Any]]) throws -> [Tweak] in + try tweaks.map { (variableKey: String, value: [String: Any]) throws -> Tweak in + try tweak(for: value, feature: featureKey, variable: variableKey) + } + .sorted { $0.variable < $1.variable } + } + .flatMap { $0 } + .sorted { $0.feature < $1.feature } + + try validate(tweaks) + + return tweaks + } + + func validate(_ tweaks: [Tweak]) throws { + let propertyNames = tweaks.map { $0.propertyName }.compactMap { $0 } + let duplicates = propertyNames.duplicates + if duplicates.count > 0 { + throw "Found duplicate 'GeneratedPropertyName': \(duplicates)" + } + } + + func type(for value: Any) throws -> String { + switch value { + case _ as String: return "String" + case let numberValue as NSNumber: return numberValue.tweakType + case _ as Bool: return "Bool" + case _ as Double: return "Double" + default: + throw "Unsupported value type \(Swift.type(of: value))" + } + } + + func tweak(for dictionary: [String: Any], feature: FeatureKey, variable: VariableKey) throws -> Tweak { + guard let title = dictionary["Title"] as? String else { + throw "Missing 'Title' value in dictionary \(dictionary)" + } + guard let group = dictionary["Group"] as? String else { + throw "Missing 'Group' value in dictionary \(dictionary)" + } + guard let value = dictionary["Value"] else { + throw "Missing 'Value' value in dictionary \(dictionary)" + } + return Tweak(feature: feature, + variable: variable, + title: title, + description: dictionary["Description"] as? String, + group: group, + valueType: try type(for: value), + propertyName: dictionary["GeneratedPropertyName"] as? String) + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakValue.swift b/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakValue.swift new file mode 100644 index 0000000..08cea5d --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Core/TweakValue.swift @@ -0,0 +1,14 @@ +// +// TweakValue.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +public protocol TweakValue: CustomStringConvertible {} + +extension Bool: TweakValue {} +extension Int: TweakValue {} +extension Float: TweakValue {} +extension Double: TweakValue {} +extension String: TweakValue {} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Core/Types.swift b/TweakAccessorGenerator/TweakAccessorGenerator/Core/Types.swift new file mode 100644 index 0000000..19f1490 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Core/Types.swift @@ -0,0 +1,15 @@ +// +// Types.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +typealias Filename = String +typealias CodeBlock = String +typealias TweaksFormat = [FeatureKey: FeatureFormat] +typealias FeatureFormat = [VariableKey: TweakFormat] +typealias TweakFormat = [String: Any] + +typealias FeatureKey = String +typealias VariableKey = String diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/Array+Duplicates.swift b/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/Array+Duplicates.swift new file mode 100644 index 0000000..e09483b --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/Array+Duplicates.swift @@ -0,0 +1,16 @@ +// +// Array+Duplicates.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +extension Array where Element: Hashable { + + var duplicates: Array { + let groups = Dictionary(grouping: self, by: { $0 }) + let duplicateGroups = groups.filter { $1.count > 1 } + let duplicates = Array(duplicateGroups.keys) + return duplicates + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/NSNumber+ValueTypes.swift b/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/NSNumber+ValueTypes.swift new file mode 100644 index 0000000..4d7b4c2 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/NSNumber+ValueTypes.swift @@ -0,0 +1,27 @@ +// +// NSNumber+ValueTypes.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +public extension NSNumber { + + var tweakType: String { + let encoding = String(cString: self.objCType) + switch encoding { + case "d": + return "Double" + + case "q": + return "Int" + + case "c": + return "Bool" + + default: + assert(false, "Unsupported objCType for NSNumber \(self)") + return "" + } + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/String+Casing.swift b/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/String+Casing.swift new file mode 100644 index 0000000..b3fd25b --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/Extensions/String+Casing.swift @@ -0,0 +1,25 @@ +// +// String+Casing.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation + +extension String { + + func camelCased(with separator: Character = "_") -> String { + self + .split(separator: separator) + .enumerated() + .map { $0.offset == 0 ? String($0.element).lowercasedFirstChar() : String($0.element).capitalisedFirstChar() } + .joined() + } + + func lowercasedFirstChar() -> String { + prefix(1).lowercased() + self.dropFirst() + } + + func capitalisedFirstChar() -> String { + prefix(1).uppercased() + self.dropFirst() + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGenerator/main.swift b/TweakAccessorGenerator/TweakAccessorGenerator/main.swift new file mode 100644 index 0000000..d8ad21e --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGenerator/main.swift @@ -0,0 +1,96 @@ +// +// main.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation +import ArgumentParser + +struct TweakAccessorGenerator: ParsableCommand { + + @Option(name: .shortAndLong, help: "The local tweaks file path.") + var localTweaksFilePath: String + + @Option(name: .shortAndLong, help: "The output folder.") + var outputFolder: String + + @Option(name: .shortAndLong, help: "The configuration folder.") + var configurationFolder: String + + private var tweaksFilename: String { + let url = URL(fileURLWithPath: localTweaksFilePath) + return String(url.lastPathComponent.split(separator: ".").first!) + } + + private var configurationFolderURL: URL { + URL(fileURLWithPath: configurationFolder) + } + + private func loadConfigurationFromJson() -> Configuration { + let configurationUrl = configurationFolderURL.appendingPathComponent("config.json") + let jsonData = try! Data(contentsOf: configurationUrl) + let decodedResult = try! JSONDecoder().decode(Configuration.self, from: jsonData) + return decodedResult + } + + private func loadCustomTweakProvidersCode() -> [Filename: CodeBlock] { + let configuration = loadConfigurationFromJson() + let customTweakProviders = configuration.tweakProviders.filter { $0.type == "CustomTweakProvider" } // costantise + let filenameReferences = customTweakProviders.map { $0.parameter! } + + var customTweakProvidersCode: [Filename: CodeBlock] = [:] + for reference in filenameReferences { + let configurationUrl = configurationFolderURL.appendingPathComponent(reference) + let content = try! String(contentsOf: configurationUrl) + customTweakProvidersCode[reference] = content + } + return customTweakProvidersCode + } + + func run() throws { + let codeGenerator = TweakAccessorCodeGenerator() + let tweakLoader = TweakLoader() + let tweaks = try tweakLoader.load(localTweaksFilePath) + let configuration = loadConfigurationFromJson() + + writeConstantsFile(codeGenerator: codeGenerator, + tweaks: tweaks, + outputFolder: outputFolder, + configuration: configuration) + + writeAccessorFile(codeGenerator: codeGenerator, + tweaks: tweaks, + outputFolder: outputFolder, + configuration: configuration) + } +} + +extension TweakAccessorGenerator { + + private func writeConstantsFile(codeGenerator: TweakAccessorCodeGenerator, + tweaks: [Tweak], + outputFolder: String, + configuration: Configuration) { + let fileName = "\(configuration.accessorName)+Constants.swift" + let url: URL = URL(fileURLWithPath: outputFolder).appendingPathComponent(fileName) + let constants = codeGenerator.generateConstantsFileContent(tweaks: tweaks, + configuration: configuration) + try! constants.write(to: url, atomically: true, encoding: .utf8) + } + + private func writeAccessorFile(codeGenerator: TweakAccessorCodeGenerator, + tweaks: [Tweak], + outputFolder: String, + configuration: Configuration) { + let customTweakProvidersSetupCode = loadCustomTweakProvidersCode() + let fileName = "\(configuration.accessorName).swift" + let url: URL = URL(fileURLWithPath: outputFolder).appendingPathComponent(fileName) + let constants = codeGenerator.generateAccessorFileContent(tweaksFilename: tweaksFilename, + tweaks: tweaks, + configuration: configuration, + customTweakProvidersSetupCode: customTweakProvidersSetupCode) + try! constants.write(to: url, atomically: true, encoding: .utf8) + } +} + +TweakAccessorGenerator.main() diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/FirebaseTweakProviderSetupCode b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/FirebaseTweakProviderSetupCode new file mode 100644 index 0000000..dd591d0 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/FirebaseTweakProviderSetupCode @@ -0,0 +1,3 @@ +let firebaseTweakProvider = FirebaseTweakProvider() +firebaseTweakProvider.someValue = true +tweakProviders.append(firebaseTweakProvider) diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/GeneratedTweakAccessor+ConstantsContent b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/GeneratedTweakAccessor+ConstantsContent new file mode 100644 index 0000000..c074c0b --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/GeneratedTweakAccessor+ConstantsContent @@ -0,0 +1,25 @@ +// +// GeneratedTweakAccessorContent+Constants.swift +// Generated by TweakAccessorGenerator +// + +import Foundation + +extension GeneratedTweakAccessorContent { + + struct Features { + static let general = "general" + static let uiCustomization = "ui_customization" + } + + struct Variables { + static let answerToTheUniverse = "answer_to_the_universe" + static let displayGreenView = "display_green_view" + static let displayRedView = "display_red_view" + static let displayYellowView = "display_yellow_view" + static let greetOnAppDidBecomeActive = "greet_on_app_did_become_active" + static let labelText = "label_text" + static let redViewAlphaComponent = "red_view_alpha_component" + static let tapToChangeColorEnabled = "tap_to_change_color_enabled" + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/GeneratedTweakAccessorContent b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/GeneratedTweakAccessorContent new file mode 100644 index 0000000..ff848ec --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/GeneratedTweakAccessorContent @@ -0,0 +1,101 @@ +// +// GeneratedTweakAccessorContent.swift +// Generated by TweakAccessorGenerator +// + +import Foundation +import JustTweak + +class GeneratedTweakAccessorContent { + + static let tweakManager: TweakManager = { + var tweakProviders: [TweakProvider] = [] + + // EphemeralTweakProvider + #if DEBUG || CONFIGURATION_UI_TESTS + let ephemeralTweakProvider_1 = NSMutableDictionary() + tweakProviders.append(ephemeralTweakProvider_1) + #endif + + // UserDefaultsTweakProvider + #if DEBUG || CONFIGURATION_DEBUG + let userDefaultsTweakProvider_1 = UserDefaultsTweakProvider(userDefaults: UserDefaults.standard) + tweakProviders.append(userDefaultsTweakProvider_1) + #endif + + // CustomTweakProvider + #if CONFIGURATION_APPSTORE + let optimizelyTweakProvider = OptimizelyTweakProvider() + optimizelyTweakProvider.someValue = 42 + tweakProviders.append(optimizelyTweakProvider) + #endif + + // CustomTweakProvider + #if CONFIGURATION_APPSTORE + let firebaseTweakProvider = FirebaseTweakProvider() + firebaseTweakProvider.someValue = true + tweakProviders.append(firebaseTweakProvider) + #endif + + // LocalTweakProvider + #if DEBUG + let jsonFileURL_1 = Bundle.main.url(forResource: "ValidTweaks_TopPriority", withExtension: "json")! + let localTweakProvider_1 = LocalTweakProvider(jsonURL: jsonFileURL_1) + tweakProviders.append(localTweakProvider_1) + #endif + + // LocalTweakProvider + let jsonFileURL_2 = Bundle.main.url(forResource: "ValidTweaks_LowPriority", withExtension: "json")! + let localTweakProvider_2 = LocalTweakProvider(jsonURL: jsonFileURL_2) + tweakProviders.append(localTweakProvider_2) + + let tweakManager = TweakManager(tweakProviders: tweakProviders) + tweakManager.useCache = true + return tweakManager + }() + + var tweakManager: TweakManager { + return Self.tweakManager + } + + @TweakProperty(feature: Features.general, + variable: Variables.answerToTheUniverse, + tweakManager: tweakManager) + var meaningOfLife: Int + + @TweakProperty(feature: Features.general, + variable: Variables.greetOnAppDidBecomeActive, + tweakManager: tweakManager) + var shouldShowAlert: Bool + + @TweakProperty(feature: Features.general, + variable: Variables.tapToChangeColorEnabled, + tweakManager: tweakManager) + var isTapGestureToChangeColorEnabled: Bool + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.displayGreenView, + tweakManager: tweakManager) + var canShowGreenView: Bool + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.displayRedView, + tweakManager: tweakManager) + var canShowRedView: Bool + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.displayYellowView, + tweakManager: tweakManager) + var canShowYellowView: Bool + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.labelText, + tweakManager: tweakManager) + var labelText: String + + @TweakProperty(feature: Features.uiCustomization, + variable: Variables.redViewAlphaComponent, + tweakManager: tweakManager) + var redViewAlpha: Double +} + diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_DuplicateGeneratedPropertyName.json b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_DuplicateGeneratedPropertyName.json new file mode 100644 index 0000000..503778a --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_DuplicateGeneratedPropertyName.json @@ -0,0 +1,26 @@ +{ + "ui_customization": { + "label_text": { + "Title": "Label Text", + "Description": "the title of the main label", + "Group": "UI Customization", + "Value": "Test value", + "GeneratedPropertyName": "definitiveAnswer" + } + }, + "general": { + "answer_to_the_universe": { + "Title": "Definitive answer", + "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General", + "Value": 42, + "GeneratedPropertyName": "definitiveAnswer" + }, + "tap_to_change_color_enabled": { + "Title": "Tap to change views color", + "Description": "change the colour of the main view when receiving a tap", + "Group": "General", + "Value": true + } + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_InvalidJSON.json b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_InvalidJSON.json new file mode 100644 index 0000000..780dade --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_InvalidJSON.json @@ -0,0 +1,11 @@ +{ + "general": { + "answer_to_the_universe": { + "Title": "Definitive answer" + "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything" + "Group": "General" + "Value": 42 + "GeneratedPropertyName": "definitiveAnswer" + } + } +} diff --git a/Example/Tests/test_configuration_no_displayable_ungrouped.json b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_MissingValues.json similarity index 52% rename from Example/Tests/test_configuration_no_displayable_ungrouped.json rename to TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_MissingValues.json index 1b3f070..4d9208d 100644 --- a/Example/Tests/test_configuration_no_displayable_ungrouped.json +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/InvalidTweaks_MissingValues.json @@ -1,9 +1,9 @@ { "ui_customization": { "display_red_view": { - "Title": "Display Red View", + "Description": "shows a red view in the main view controller", "Group": "UI Customization", - "Value": true + "Value": false }, "display_yellow_view": { "Title": "Display Yellow View", @@ -12,20 +12,15 @@ }, "display_green_view": { "Title": "Display Green View", - "Group": "UI Customization", + "Description": "shows a green view in the main view controller", "Value": true } }, "general": { - "greet_on_app_did_become_active": { - "Title": "Greet on app launch", - "Group": "General", - "Value": false - }, - "tap_to_change_color_enabled": { - "Title": "Tap to change views color", - "Group": "General", - "Value": true + "answer_to_the_universe": { + "Title": "Definitive answer", + "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General" } } } diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/OptimizelyTweakProviderSetupCode b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/OptimizelyTweakProviderSetupCode new file mode 100644 index 0000000..209ec24 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/OptimizelyTweakProviderSetupCode @@ -0,0 +1,3 @@ +let optimizelyTweakProvider = OptimizelyTweakProvider() +optimizelyTweakProvider.someValue = 42 +tweakProviders.append(optimizelyTweakProvider) diff --git a/Example/JustTweak/ExampleConfiguration.json b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/Tweaks.json similarity index 84% rename from Example/JustTweak/ExampleConfiguration.json rename to TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/Tweaks.json index 3e9340b..2aea73a 100644 --- a/Example/JustTweak/ExampleConfiguration.json +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/Tweaks.json @@ -32,6 +32,13 @@ } }, "general": { + "answer_to_the_universe": { + "Title": "Definitive answer", + "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General", + "Value": 42, + "GeneratedPropertyName": "definitiveAnswer" + }, "greet_on_app_did_become_active": { "Title": "Greet on app launch", "Description": "shows an alert on applicationDidBecomeActive", diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/ValidTweaks_LowPriority.json b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/ValidTweaks_LowPriority.json new file mode 100644 index 0000000..25b12f7 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/ValidTweaks_LowPriority.json @@ -0,0 +1,61 @@ +{ + "ui_customization": { + "display_red_view": { + "Title": "Display Red View", + "Description": "shows a red view in the main view controller", + "Group": "UI Customization", + "Value": false, + "GeneratedPropertyName": "canShowRedView" + }, + "display_yellow_view": { + "Title": "Display Yellow View", + "Description": "shows a yellow view in the main view controller", + "Group": "UI Customization", + "Value": false, + "GeneratedPropertyName": "canShowYellowView" + }, + "display_green_view": { + "Title": "Display Green View", + "Description": "shows a green view in the main view controller", + "Group": "UI Customization", + "Value": true, + "GeneratedPropertyName": "canShowGreenView" + }, + "red_view_alpha_component": { + "Title": "Red View Alpha Component", + "Description": "defines the alpha level of the red view", + "Group": "UI Customization", + "Value": 1.0, + "GeneratedPropertyName": "redViewAlpha" + }, + "label_text": { + "Title": "Label Text", + "Description": "the title of the main label", + "Group": "UI Customization", + "Value": "Test value" + } + }, + "general": { + "answer_to_the_universe": { + "Title": "Definitive answer", + "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General", + "Value": 42, + "GeneratedPropertyName": "meaningOfLife" + }, + "greet_on_app_did_become_active": { + "Title": "Greet on app launch", + "Description": "shows an alert on applicationDidBecomeActive", + "Group": "General", + "Value": false, + "GeneratedPropertyName": "shouldShowAlert" + }, + "tap_to_change_color_enabled": { + "Title": "Tap to change views color", + "Description": "change the colour of the main view when receiving a tap", + "Group": "General", + "Value": true, + "GeneratedPropertyName": "isTapGestureToChangeColorEnabled" + } + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/ValidTweaks_TopPriority.json b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/ValidTweaks_TopPriority.json new file mode 100644 index 0000000..9261c53 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Assets/ValidTweaks_TopPriority.json @@ -0,0 +1,31 @@ +{ + "ui_customization": { + "display_red_view": { + "Title": "Display Red View", + "Description": "shows a red view in the main view controller", + "Group": "UI Customization", + "Value": true + }, + "display_yellow_view": { + "Title": "Display Yellow View", + "Description": "shows a yellow view in the main view controller", + "Group": "UI Customization", + "Value": true + }, + "display_green_view": { + "Title": "Display Green View", + "Description": "shows a green view in the main view controller", + "Group": "UI Customization", + "Value": false + } + }, + "general": { + "answer_to_the_universe": { + "Title": "Definitive answer", + "Description": "Answer to the Ultimate Question of Life, the Universe, and Everything", + "Group": "General", + "Value": 42, + "GeneratedPropertyName": "definitiveAnswer" + } + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Info.plist b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/Array+DuplicatesTests.swift b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/Array+DuplicatesTests.swift new file mode 100644 index 0000000..2863679 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/Array+DuplicatesTests.swift @@ -0,0 +1,21 @@ +// +// Array+DuplicatesTests.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import XCTest + +class Array_DuplicatesTests: XCTestCase { + + func test_noDuplicatesFound() { + let arrayWithNoDuplicates = ["some", "array", "with", "no", "duplicates"] + let expectedValue: [String] = [] + XCTAssertEqual(arrayWithNoDuplicates.duplicates, expectedValue) + } + + func test_duplicatesFound() { + let arrayWithDuplicates = ["some", "array", "with", "some", "duplicates", "here", "here", "and", "here"] + let expectedValue = ["here", "some"] + XCTAssertEqual(arrayWithDuplicates.duplicates.sorted(), expectedValue) + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/NSNumber+ValueTypesTests.swift b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/NSNumber+ValueTypesTests.swift new file mode 100644 index 0000000..88daaf8 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/NSNumber+ValueTypesTests.swift @@ -0,0 +1,35 @@ +// +// NSNumber+ValueTypesTests.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import Foundation +import XCTest + +class NSNumber_ValueTypesTests: XCTestCase { + + func test_tweakType_for_NSNumber_with_Int_value() { + let sut = NSNumber(value: 42) + XCTAssertEqual(sut.tweakType, "Int") + } + + func test_tweakType_for_NSNumber_with_IntegerLiteral_value() { + let sut2 = NSNumber(integerLiteral: 42) + XCTAssertEqual(sut2.tweakType, "Int") + } + + func test_tweakType_for_NSNumber_with_Double_value() { + let sut = NSNumber(value: 3.14) + XCTAssertEqual(sut.tweakType, "Double") + } + + func test_tweakType_for_NSNumber_with_FloatLiteral_value() { + let sut = NSNumber(floatLiteral: 3.14) + XCTAssertEqual(sut.tweakType, "Double") + } + + func test_tweakType_for_NSNumber_with_Bool_value() { + let sut = NSNumber(value: true) + XCTAssertEqual(sut.tweakType, "Bool") + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/String+CasingTests.swift b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/String+CasingTests.swift new file mode 100644 index 0000000..7dae2e5 --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/String+CasingTests.swift @@ -0,0 +1,39 @@ +// +// String+CasingTests.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import XCTest + +class String_CasingTests: XCTestCase { + + func test_camelCased_with_default_separator_1() { + let sut = "Some-Property_Key_some-Value_some" + let expectedValue = "some-PropertyKeySome-ValueSome" + XCTAssertEqual(expectedValue, sut.camelCased()) + } + + func test_camelCased_with_default_separator_2() { + let sut = "Some_PropertyKey" + let expectedValue = "somePropertyKey" + XCTAssertEqual(expectedValue, sut.camelCased()) + } + + func test_camelCased_with_custom_separator() { + let sut = "Some-Property:Key:some-Value:Some" + let expectedValue = "some-PropertyKeySome-ValueSome" + XCTAssertEqual(expectedValue, sut.camelCased(with: ":")) + } + + func test_lowercasedFirstChar() { + let sut = "SomeName" + let expectedValue = "someName" + XCTAssertEqual(expectedValue, sut.lowercasedFirstChar()) + } + + func test_capitalisedFirstChar() { + let sut = "someName" + let expectedValue = "SomeName" + XCTAssertEqual(expectedValue, sut.capitalisedFirstChar()) + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakAccessorCodeGeneratorTests.swift b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakAccessorCodeGeneratorTests.swift new file mode 100644 index 0000000..eb7de5e --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakAccessorCodeGeneratorTests.swift @@ -0,0 +1,88 @@ +// +// TweakAccessorCodeGeneratorTests.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import XCTest + +class TweakAccessorCodeGeneratorTests: XCTestCase { + + private var bundle: Bundle! + private let tweaksFilename = "ValidTweaks_LowPriority" + private var tweaksFilePath: String! + private let generatedClassName = "GeneratedTweakAccessor" + private var codeGenerator: TweakAccessorCodeGenerator! + private var tweakLoader: TweakLoader! + private var tweaks: [Tweak]! + + override func setUpWithError() throws { + bundle = Bundle(for: type(of: self)) + tweaksFilePath = bundle.path(forResource: tweaksFilename, ofType: "json")! + codeGenerator = TweakAccessorCodeGenerator() + tweakLoader = TweakLoader() + tweaks = try tweakLoader.load(tweaksFilePath) + } + + override func tearDownWithError() throws { + bundle = nil + tweaksFilePath = nil + codeGenerator = nil + tweakLoader = nil + tweaks = nil + } + + func test_generateConstants_output() throws { + let configuration = Configuration(tweakProviders: [], + shouldCacheTweaks: true, + accessorName: "GeneratedTweakAccessorContent") + let content = codeGenerator.generateConstantsFileContent(tweaks: tweaks, configuration: configuration) + let testContentPath = bundle.path(forResource: "GeneratedTweakAccessor+ConstantsContent", ofType: "")! + let testContent = try String(contentsOfFile: testContentPath, encoding: .utf8).trimmingCharacters(in: .newlines) + XCTAssertEqual(content, testContent) + } + + func test_generateAccessor_output() throws { + let tweakProviders = [ + TweakProvider(type: "EphemeralTweakProvider", + parameter: nil, + macros: ["DEBUG", "CONFIGURATION_UI_TESTS"]), + TweakProvider(type: "UserDefaultsTweakProvider", + parameter: "UserDefaults.standard", + macros: ["DEBUG", "CONFIGURATION_DEBUG"]), + TweakProvider(type: "CustomTweakProvider", + parameter: "OptimizelyTweakProviderSetupCode", + macros: ["CONFIGURATION_APPSTORE"]), + TweakProvider(type: "CustomTweakProvider", + parameter: "FirebaseTweakProviderSetupCode", + macros: ["CONFIGURATION_APPSTORE"]), + TweakProvider(type: "LocalTweakProvider", + parameter: "ValidTweaks_TopPriority", + macros: ["DEBUG"]), + TweakProvider(type: "LocalTweakProvider", + parameter: "ValidTweaks_LowPriority", + macros: nil) + ] + let configuration = Configuration(tweakProviders: tweakProviders, + shouldCacheTweaks: true, + accessorName: "GeneratedTweakAccessorContent") + let customTweakProvidersSetupCode = [ + "FirebaseTweakProviderSetupCode": codeBlock(for: "FirebaseTweakProviderSetupCode"), + "OptimizelyTweakProviderSetupCode": codeBlock(for: "OptimizelyTweakProviderSetupCode"), + + ] + let content = codeGenerator.generateAccessorFileContent(tweaksFilename: tweaksFilename, + tweaks: tweaks, + configuration: configuration, + customTweakProvidersSetupCode: customTweakProvidersSetupCode) + let testContentPath = bundle.path(forResource: "GeneratedTweakAccessorContent", ofType: "")! + let testContent = try String(contentsOfFile: testContentPath, encoding: .utf8).trimmingCharacters(in: .newlines) + + XCTAssertEqual(content, testContent) + } + + private func codeBlock(for customTweakProviderFile: String) -> String { + let testBundle = Bundle(for: TweakAccessorCodeGeneratorTests.self) + let filePath = testBundle.path(forResource: customTweakProviderFile, ofType: "")! + return try! String(contentsOfFile: filePath) + } +} diff --git a/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakLoaderTests.swift b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakLoaderTests.swift new file mode 100644 index 0000000..cc890be --- /dev/null +++ b/TweakAccessorGenerator/TweakAccessorGeneratorTests/Suites/TweakLoaderTests.swift @@ -0,0 +1,223 @@ +// +// TweakLoaderTests.swift +// Copyright © 2021 Just Eat Takeaway. All rights reserved. +// + +import XCTest + +class TweakLoaderTests: XCTestCase { + + var sut: TweakLoader! + + override func setUp() { + super.setUp() + sut = TweakLoader() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + func test_loadConfiguration_success() throws { + let bundle = Bundle(for: type(of: self)) + let tweaksFilename = "Tweaks" + let tweaksFilePath = bundle.path(forResource: tweaksFilename, ofType: "json")! + + let testTweaks = try sut.load(tweaksFilePath) + + let expectedTweaks = [ + Tweak(feature: "general", + variable: "answer_to_the_universe", + title: "Definitive answer", + description: "Answer to the Ultimate Question of Life, the Universe, and Everything", + group: "General", + valueType: "Int", + propertyName: "definitiveAnswer"), + Tweak(feature: "general", + variable: "greet_on_app_did_become_active", + title: "Greet on app launch", + description: "shows an alert on applicationDidBecomeActive", + group: "General", + valueType: "Bool", + propertyName: nil), + Tweak(feature: "general", + variable: "tap_to_change_color_enabled", + title: "Tap to change views color", + description: "change the colour of the main view when receiving a tap", + group: "General", + valueType: "Bool", + propertyName: nil), + Tweak(feature: "ui_customization", + variable: "display_green_view", + title: "Display Green View", + description: "shows a green view in the main view controller", + group: "UI Customization", + valueType: "Bool", + propertyName: nil), + Tweak(feature: "ui_customization", + variable: "display_red_view", + title: "Display Red View", + description: "shows a red view in the main view controller", + group: "UI Customization", + valueType: "Bool", + propertyName: nil), + Tweak(feature: "ui_customization", + variable: "display_yellow_view", + title: "Display Yellow View", + description: "shows a yellow view in the main view controller", + group: "UI Customization", + valueType: "Bool", + propertyName: nil), + Tweak(feature: "ui_customization", + variable: "label_text", + title: "Label Text", + description: "the title of the main label", + group: "UI Customization", + valueType: "String", + propertyName: nil), + Tweak(feature: "ui_customization", + variable: "red_view_alpha_component", + title: "Red View Alpha Component", + description: "defines the alpha level of the red view", + group: "UI Customization", + valueType: "Double", + propertyName: nil)] + XCTAssertEqual(testTweaks, expectedTweaks) + } + + func test_loadConfiguration_failure_invalidJSON() throws { + let bundle = Bundle(for: type(of: self)) + let tweaksFilename = "InvalidTweaks_InvalidJSON" + let tweaksFilePath = bundle.path(forResource: tweaksFilename, ofType: "json")! + + XCTAssertThrowsError(try sut.load(tweaksFilePath)) + } + + func test_loadConfiguration_failure_missingValues() throws { + let bundle = Bundle(for: type(of: self)) + let tweaksFilename = "InvalidTweaks_MissingValues" + let tweaksFilePath = bundle.path(forResource: tweaksFilename, ofType: "json")! + + XCTAssertThrowsError(try sut.load(tweaksFilePath)) + } + + func test_loadConfiguration_failure_duplicateGeneratedPropertyName() throws { + let bundle = Bundle(for: type(of: self)) + let tweaksFilename = "InvalidTweaks_DuplicateGeneratedPropertyName" + let tweaksFilePath = bundle.path(forResource: tweaksFilename, ofType: "json")! + XCTAssertThrowsError(try sut.load(tweaksFilePath)) + } + + func test_typeForValue_String() { + let expectedValue = "String" + XCTAssertEqual(try sut.type(for: "some string"), expectedValue) + } + + func test_typeForValue_NSNumber_Double() { + let expectedValue = "Double" + XCTAssertEqual(try sut.type(for: NSNumber(value: 3.14)), expectedValue) + } + + func test_typeForValue_NSNumber_Int() { + let expectedValue = "Int" + XCTAssertEqual(try sut.type(for: NSNumber(value: 42)), expectedValue) + } + + func test_typeForValue_NSNumber_Bool() { + let expectedValue = "Bool" + XCTAssertEqual(try sut.type(for: NSNumber(value: true)), expectedValue) + } + + func test_typeForValue_Bool() { + let expectedValue = "Bool" + XCTAssertEqual(try sut.type(for: true), expectedValue) + } + + func test_typeForValue_Double() { + let expectedValue = "Double" + XCTAssertEqual(try sut.type(for: 3.14), expectedValue) + } + + func test_typeForValue_Unsupported() { + XCTAssertThrowsError(try sut.type(for: [""])) + } + + func test_tweakForDictionary_AllRequiredValuesPresent() throws { + let feature = "some feature" + let variable = "some variable" + let title = "some title" + let description = "some description" + let group = "some group" + let dictionary: [String : Any] = [ + "Title": title, + "Description": description, + "Group": group, + "Value": 3.14 + ] + let expectedValue = Tweak(feature: feature, + variable: variable, + title: title, + description: description, + group: group, + valueType: "Double", + propertyName: nil) + let testValue = try sut.tweak(for: dictionary, + feature: feature, + variable: variable) + XCTAssertEqual(testValue, expectedValue) + } + + func test_tweakForDictionary_MissingTitle() { + let feature = "some feature" + let variable = "some variable" + let description = "some description" + let group = "some group" + let dictionary: [String : Any] = [ + "Description": description, + "Group": group, + "Value": 3.14 + ] + XCTAssertThrowsError(try sut.tweak(for: dictionary, feature: feature, variable: variable)) + } + + func test_tweakForDictionary_MissingDescription() { + let feature = "some feature" + let variable = "some variable" + let title = "some title" + let group = "some group" + let dictionary: [String : Any] = [ + "Title": title, + "Group": group, + "Value": 3.14 + ] + XCTAssertNoThrow(try sut.tweak(for: dictionary, feature: feature, variable: variable)) + } + + func test_tweakForDictionary_MissingGroup() { + let feature = "some feature" + let variable = "some variable" + let title = "some title" + let description = "some description" + let dictionary: [String : Any] = [ + "Title": title, + "Description": description, + "Value": 3.14 + ] + XCTAssertThrowsError(try sut.tweak(for: dictionary, feature: feature, variable: variable)) + } + + func test_tweakForDictionary_MissingValue() { + let feature = "some feature" + let variable = "some variable" + let title = "some title" + let description = "some description" + let group = "some group" + let dictionary: [String : Any] = [ + "Title": title, + "Description": description, + "Group": group + ] + XCTAssertThrowsError(try sut.tweak(for: dictionary, feature: feature, variable: variable)) + } +}