diff --git a/mopidy/utils/config.py b/mopidy/utils/config.py index dc813678..10f1bf78 100644 --- a/mopidy/utils/config.py +++ b/mopidy/utils/config.py @@ -17,3 +17,56 @@ def validate_maximum(value, maximum): """Maximum validation, normally called in config value's validate().""" if maximum is not None and value > maximum: raise ValueError('must be smaller than %s.' % maximum) + + +class ConfigValue(object): + """Represents a config key's value and how to handle it. + + Normally you will only be interacting with sub-classes for config values + that encode either deserialization behavior and/or validation. + + Each config value should be used for the following actions: + + 1. Deserializing from a raw string and validating, raising ValueError on + failure. + 2. Serializing a value back to a string that can be stored in a config. + 3. Formatting a value to a printable form (useful for masking secrets). + + :class:`None` values should not be deserialized, serialized or formatted, + the code interacting with the config should simply skip None config values. + """ + + #: Collection of valid choices for converted value. Must be combined with + #: :function:`validate_choices` in :method:`validate` do any thing. + choices = None + + #: Minimum of converted value. Must be combined with + #: :function:`validate_minimum` in :method:`validate` do any thing. + minimum = None + + #: Maximum of converted value. Must be combined with + #: :function:`validate_maximum` in :method:`validate` do any thing. + maximum = None + + #: Indicate if we should mask the when printing for human consumption. + secret = None + + def __init__(self, choices=None, minimum=None, maximum=None, secret=None): + self.choices = choices + self.minimum = minimum + self.maximum = maximum + self.secret = secret + + def deserialize(self, value): + """Cast raw string to appropriate type.""" + return value + + def serialize(self, value): + """Convert value back to string for saving.""" + return str(value) + + def format(self, value): + """Format value for display.""" + if self.secret: + return '********' + return self.serialize(value) diff --git a/tests/utils/config_test.py b/tests/utils/config_test.py index 4c8c28a2..c1572c78 100644 --- a/tests/utils/config_test.py +++ b/tests/utils/config_test.py @@ -51,3 +51,38 @@ class ValidateMaximumTest(unittest.TestCase): def test_to_large_value_fails_with_zero_as_maximum(self): with self.assertRaises(ValueError): config.validate_maximum(5, 0) + + +class ConfigValueTest(unittest.TestCase): + def test_init(self): + value = config.ConfigValue() + self.assertIsNone(value.choices) + self.assertIsNone(value.minimum) + self.assertIsNone(value.maximum) + self.assertIsNone(value.secret) + + def test_init_with_params(self): + value = config.ConfigValue( + choices=['foo'], minimum=0, maximum=10, secret=True) + self.assertEqual(['foo'], value.choices) + self.assertEqual(0, value.minimum) + self.assertEqual(10, value.maximum) + self.assertEqual(True, value.secret) + + def test_deserialize_passes_through(self): + value = config.ConfigValue() + obj = object() + self.assertEqual(obj, value.deserialize(obj)) + + def test_serialize_converts_to_string(self): + value = config.ConfigValue() + self.assertIsInstance(value.serialize(object()), basestring) + + def test_format_uses_serialize(self): + value = config.ConfigValue() + obj = object() + self.assertEqual(value.serialize(obj), value.format(obj)) + + def test_format_masks_secrets(self): + value = config.ConfigValue(secret=True) + self.assertEqual('********', value.format(object()))