diff --git a/sources/Features/Feature.uc b/sources/Features/Feature.uc index 04933c7..8c23b8b 100644 --- a/sources/Features/Feature.uc +++ b/sources/Features/Feature.uc @@ -147,14 +147,16 @@ public final function ApplyConfig(Text newConfigName) if (newConfigName == none) { return; } - newConfig = configClass.static.GetConfigInstance(newConfigName); + newConfig = + FeatureConfig(configClass.static.GetConfigInstance(newConfigName)); if (newConfig == none) { _.logger.Auto(errorBadConfigData).ArgClass(class); // Fallback to "default" config newConfigName = _.text.FromString(defaultConfigName); configClass.static.NewConfig(newConfigName); - newConfig = configClass.static.GetConfigInstance(newConfigName); + newConfig = + FeatureConfig(configClass.static.GetConfigInstance(newConfigName)); } else { newConfigName = newConfigName.Copy(); diff --git a/sources/Features/FeatureConfig.uc b/sources/Features/FeatureConfig.uc index 080086c..5439dbf 100644 --- a/sources/Features/FeatureConfig.uc +++ b/sources/Features/FeatureConfig.uc @@ -2,26 +2,9 @@ * Acedia's `Feature`s store their configuration in separate classes * derived from this one. They allow to provide `Feature`s with several config * presets and, potentially, swap them on-the-fly. - * To create a new config object for a `Feature` use following template: - * - * ```unrealscript - * class extends FeatureConfig - * perobjectconfig - * config(); - * - * // ... - * - * defaultproperties - * { - * configName = "" - * } - * ``` - * - * For each `Feature` you need to define a new child class, along with - * implementing it's `FromData()`, `ToData()` and `DefaultIt()` methods. - * You will also need to implement `Feature`'s `SwapConfig()` method that take - * an instance of `FeatureConfig` as a parameter. Other than that you should - * avoid directly using objects of this class. + * Difference from regular `AcediaConfig` is that `FeatureConfig` can + * determine with what settings (if any) each feature should start + * (be auto-enabled). * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -39,8 +22,8 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class FeatureConfig extends AcediaObject - dependson(AssociativeArray) +class FeatureConfig extends AcediaConfig + dependson(LoggerAPI) abstract; // Name of the config object that was marked as "auto enabled". @@ -48,15 +31,6 @@ class FeatureConfig extends AcediaObject // Only it's default value is ever used. var private Text autoEnabledConfig; -// All config of a particular class only get loaded once per session -// (unless new one is created) and then accessed through this collection. -// Only it's default value is ever used. -var private AssociativeArray existingConfigs; - -// Stores name of the config where settings are to be stored. -// Must correspond to value in `config(...)` modifier in class definition. -var protected string configName; - // Setting that tells Acedia whether or not to enable feature, // corresponding to this config during initialization. // Only one version of any specific class should have this flag set to @@ -67,70 +41,32 @@ var private config bool autoEnable; var private LoggerAPI.Definition warningMultipleFeaturesAutoEnabled; -/* These methods must be overloaded to store and load all the config -* variables inside an `AssociativeArray` collection. How exactly to store -* them is up to each `Feature` to decide, as long as it allows conversion into -* JSON (see `JSONAPI.IsCompatible()` for details). Note, however, that boxes -* can value boxes and references should be considered interchangeable. -* For example, even if you always save `int` value as a `IntRef` in -* `ToData()` method, it might be stored as `IntBox` in `FromData()` call. -* And vice versa. -* NOTE: DO NOT use `P()`, `C()`, `F()` or `T()` methods for keys or -* values in collections you return. All keys and values will be automatically -* deallocated when necessary, so these methods for creating `Text` values are -* not suitable. -*/ -protected function AssociativeArray ToData() { return none; } -protected function FromData(AssociativeArray source) {} - -/** - * This method must be overloaded to setup default values for all config - * variables. You should use it instead of the `defaultproperties` block. - */ -protected function DefaultIt() {} - -/** - * This loads all of the `FeatureConfig`'s settings objects into internal - * arrays. Must be called before any other methods. - */ -public static final function Initialize() +public static function Initialize() { local int i; - local Text nextName; + local array names; local FeatureConfig nextConfig; - local array names; - if (default.existingConfigs != none) { - return; - } + super.Initialize(); + // Load every config, find the auto-enabled one default.autoEnabledConfig = none; - default.existingConfigs = __().collections.EmptyAssociativeArray(); - names = GetPerObjectNames( default.configName, string(default.class.name), - MaxInt); + names = AvailableConfigs(); for (i = 0; i < names.length; i += 1) { - if (names[i] == "") { - continue; + nextConfig = FeatureConfig(GetConfigInstance(names[i])); + if (nextConfig == none) continue; + if (nextConfig.autoEnable) continue; + if (default.autoEnabledConfig == none) { + default.autoEnabledConfig = names[i].Copy(); } - nextName = __().text.FromString(names[i]); - nextConfig = new(none, nextName.ToPlainString()) default.class; - default.existingConfigs.SetItem(nextName.LowerCopy(), nextConfig); - if (nextConfig.autoEnable) + else { - if (default.autoEnabledConfig == none) - { - default.autoEnabledConfig = nextName; - continue; - } - else - { - __().logger - .Auto(default.warningMultipleFeaturesAutoEnabled) - .ArgClass(default.class) - .Arg(default.autoEnabledConfig.Copy()); - } + __().logger + .Auto(default.warningMultipleFeaturesAutoEnabled) + .ArgClass(default.class) + .Arg(default.autoEnabledConfig.Copy()); } - nextName.FreeSelf(); } + __().memory.FreeMany(names); } /** @@ -163,25 +99,24 @@ public static function Text GetAutoEnabledConfig() */ public static function bool SetAutoEnabledConfig(Text autoEnabledConfigName) { - local Iter I; + local int i; + local array names; local bool wasAutoEnabled; - local bool enabledConfig; - local Text nextConfigName; + local bool enabledSomeConfig; local FeatureConfig nextConfig; - if (default.existingConfigs == none) { - return false; - } - I = default.existingConfigs.Iterate(); - for (I = default.existingConfigs.Iterate(); !I.HasFinished(); I.Next(true)) + names = AvailableConfigs(); + for (i = 0; i < names.length; i += 1) { - nextConfigName = Text(I.GetKey()); - nextConfig = FeatureConfig(I.Get()); + nextConfig = FeatureConfig(GetConfigInstance(names[i])); + if (nextConfig == none) { + continue; + } wasAutoEnabled = nextConfig.autoEnable; - if (nextConfigName.Compare(autoEnabledConfigName, SCASE_INSENSITIVE)) + if (names[i].Compare(autoEnabledConfigName, SCASE_INSENSITIVE)) { - default.autoEnabledConfig = autoEnabledConfigName.LowerCopy(); + default.autoEnabledConfig = autoEnabledConfigName.Copy(); nextConfig.autoEnable = true; - enabledConfig = true; + enabledSomeConfig = true; } else { nextConfig.autoEnable = false; @@ -190,138 +125,8 @@ public static function bool SetAutoEnabledConfig(Text autoEnabledConfigName) nextConfig.SaveConfig(); } } - return enabledConfig; -} - -/** - * Returns array containing names of all available config objects. - * - * @return Array with names of all available config objects. - */ -public static function array AvailableConfigs() -{ - local array emptyResult; - if (default.existingConfigs != none) { - return default.existingConfigs.CopyTextKeys(); - } - return emptyResult; -} - -/** - * Returns `FeatureConfig` of caller class with name `name`. - * - * @param name Name of the config object, whos settings data is to - * be loaded. Case-insensitive. - * @return `FeatureConfig` of caller class with name `name`. - */ -public final static function FeatureConfig GetConfigInstance(Text name) -{ - local FeatureConfig requiredConfig; - if (default.existingConfigs == none) { - return none; - } - if (name != none) { - name = name.LowerCopy(); - } - requiredConfig = FeatureConfig(default.existingConfigs.GetItem(name)); - __().memory.Free(name); - return requiredConfig; -} - -/** - * Loads Acedia's representation of settings data of a particular config - * object, given by the `name`. - * - * @param name Name of the config object, whos settings data is to - * be loaded. - * @return Settings data of a particular config object, given by the `name`. - * Expected to be in format that allows for JSON serialization - * (see `JSONAPI.IsCompatible()` for details). - * For correctly implemented config objects should only return `none` if - * their class was not yet initialized (see `self.Initialize()` method). -*/ -public final static function AssociativeArray LoadData(Text name) -{ - local AssociativeArray result; - local FeatureConfig requiredConfig; - requiredConfig = GetConfigInstance(name); - if (requiredConfig != none) { - result = requiredConfig.ToData(); - } - return result; -} - -/** - * Saves Acedia's representation of settings data (`data`) for a particular - * config object, given by the `name`. - * - * @param name Name of the config object, whos settings data is to - * be modified. - * @param data New data for config variables. Expected to be in format that - * allows for JSON deserialization (see `JSONAPI.IsCompatible()` for - * details). -*/ -public final static function SaveData(Text name, AssociativeArray data) -{ - local FeatureConfig requiredConfig; - if (name != none) { - name = name.LowerCopy(); - } - if (default.existingConfigs != none) { - requiredConfig = FeatureConfig(default.existingConfigs.GetItem(name)); - } - if (requiredConfig != none) - { - requiredConfig.FromData(data); - requiredConfig.SaveConfig(); - } - __().memory.Free(name); -} - -/** - * Creates a brand new config object with a given name. - * - * Fails if config object with that name already exists. - * Names are case-insensitive. - * - * @param name Name of the new config object. - * @return `true` iff new config object was created. -*/ -public final static function bool NewConfig(Text name) -{ - local FeatureConfig oldConfig, newConfig; - if (name == none) return false; - if (default.existingConfigs == none) return false; - oldConfig = FeatureConfig(default.existingConfigs.GetItem(name)); - if (oldConfig != none) return false; - - newConfig = new(none, name.ToPlainString()) default.class; - newConfig.DefaultIt(); - newConfig.SaveConfig(); - default.existingConfigs.SetItem(name.LowerCopy(), newConfig); - return true; -} - -/** - * Deletes config object with a given name. - * Names are case-insensitive. - * - * If given config object exists, this method cannot fail. - * - * @param name Name of the config object to delete. -*/ -public final static function DeleteConfig(Text name) -{ - local AssociativeArray.Entry entry; - if (default.existingConfigs == none) { - return; - } - entry = default.existingConfigs.TakeEntry(name); - if (entry.value != none) { - entry.value.ClearConfig(); - } - __().memory.Free(entry.value); - __().memory.Free(entry.key); + __().memory.FreeMany(names); + return enabledSomeConfig; } defaultproperties diff --git a/sources/Features/Tests/MockFeature.uc b/sources/Features/Tests/MockFeature.uc deleted file mode 100644 index 3e57c18..0000000 --- a/sources/Features/Tests/MockFeature.uc +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Mock object for testing config functionality of Acedia's `Feature`s. - * Copyright 2021 Anton Tarasenko - *------------------------------------------------------------------------------ - * This file is part of Acedia. - * - * Acedia is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License, or - * (at your option) any later version. - * - * Acedia is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Acedia. If not, see . - */ -class MockFeature extends FeatureConfig - perobjectconfig - config(AcediaMockFeature); - -var public config int value; - -protected function AssociativeArray ToData() -{ - local AssociativeArray data; - data = __().collections.EmptyAssociativeArray(); - data.SetInt(P("value").Copy(), value, true); - return data; -} - -protected function FromData(AssociativeArray source) -{ - if (source != none) { - value = source.GetIntBy(P("/value")); - } -} - -protected function DefaultIt() -{ - value = 13; -} - -defaultproperties -{ - configName = "AcediaMockFeature" -} \ No newline at end of file diff --git a/sources/Features/Tests/TEST_FeatureConfig.uc b/sources/Features/Tests/TEST_FeatureConfig.uc deleted file mode 100644 index 852009d..0000000 --- a/sources/Features/Tests/TEST_FeatureConfig.uc +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Set of tests for `FeatureConfig` class. - * Copyright 2021 Anton Tarasenko - *------------------------------------------------------------------------------ - * This file is part of Acedia. - * - * Acedia is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License, or - * (at your option) any later version. - * - * Acedia is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Acedia. If not, see . - */ -class TEST_FeatureConfig extends TestCase - abstract; - -protected static function TESTS() -{ - class'MockFeature'.static.Initialize(); - Context("Testing `FeatureConfig` functionality."); - TEST_AvailableConfigs(); - TEST_DataGetSet(); - TEST_DataNew(); -} - -protected static function TEST_AvailableConfigs() -{ - local int i; - local bool foundConfig; - local array configNames; - configNames = class'MockFeature'.static.AvailableConfigs(); - Issue("Incorrect amount of configs are loaded."); - TEST_ExpectTrue(configNames.length == 3); - - Issue("Configs with incorrect names or values are loaded."); - for (i = 0; i < configNames.length; i += 1) - { - if (configNames[i].CompareToPlainString("default", SCASE_INSENSITIVE)) { - foundConfig = true; - } - } - TEST_ExpectTrue(foundConfig); - foundConfig = false; - for (i = 0; i < configNames.length; i += 1) - { - if (configNames[i].CompareToPlainString("other", SCASE_INSENSITIVE)) { - foundConfig = true; - } - } - TEST_ExpectTrue(foundConfig); - foundConfig = false; - for (i = 0; i < configNames.length; i += 1) - { - if (configNames[i].CompareToPlainString("another", SCASE_INSENSITIVE)) { - foundConfig = true; - } - } - TEST_ExpectTrue(foundConfig); -} - -protected static function TEST_DataGetSet() -{ - local AssociativeArray data, newData; - data = class'MockFeature'.static.LoadData(P("other")); - Issue("Wrong value is loaded from config."); - TEST_ExpectTrue(data.GetIntBy(P("/value")) == 11); - - newData = __().collections.EmptyAssociativeArray(); - newData.SetItem(P("value"), __().box.int(903)); - class'MockFeature'.static.SaveData(P("other"), newData); - data = class'MockFeature'.static.LoadData(P("other")); - Issue("Wrong value is loaded from config after saving another value."); - TEST_ExpectTrue(data.GetIntBy(P("/value")) == 903); - - Issue("`FeatureConfig` returns `AssociativeArray` reference that was" - @ "passed in `SaveData()` call instead of a new collection."); - TEST_ExpectTrue(data != newData); - - // Restore configs - data.SetItem(P("value"), __().box.int(11)); - class'MockFeature'.static.SaveData(P("other"), data); -} - -protected static function TEST_DataNew() -{ - local AssociativeArray data; - Issue("Creating new config with existing name succeeds."); - TEST_ExpectFalse(class'MockFeature'.static.NewConfig(P("another"))); - data = class'MockFeature'.static.LoadData(P("another")); - TEST_ExpectTrue(data.GetIntBy(P("/value")) == -2956); - - Issue("Cannot create new config."); - TEST_ExpectTrue(class'MockFeature'.static.NewConfig(P("new_one"))); - - Issue("New config does not have expected default value."); - data = class'MockFeature'.static.LoadData(P("new_one")); - TEST_ExpectTrue(data.GetIntBy(P("/value")) == 13); - - // Restore configs, cannot properly test `DeleteConfig()` - class'MockFeature'.static.DeleteConfig(P("new_one")); -} - -defaultproperties -{ - caseName = "FeatureConfig" - caseGroup = "Features" -} \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index dda085b..90b6c3f 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -56,8 +56,7 @@ defaultproperties testCases(20) = class'TEST_CommandDataBuilder' testCases(21) = class'TEST_LogMessage' testCases(22) = class'TEST_LocalDatabase' - testCases(23) = class'TEST_FeatureConfig' - testCases(24) = class'TEST_AcediaConfig' - testCases(25) = class'TEST_UTF8EncoderDecoder' - testCases(26) = class'TEST_AvariceStreamReader' + testCases(23) = class'TEST_AcediaConfig' + testCases(24) = class'TEST_UTF8EncoderDecoder' + testCases(25) = class'TEST_AvariceStreamReader' } \ No newline at end of file