Repository: keleshev/schema

Issue:
XOr implementation
First of all, this library is awesome.

I've implemented a `XOr` class for exclusive schemas (shamelessly copying from the `Or` class). Maybe this is naively wrong, but so far this has worked as expected.

``` python
class XOr(And):

    def validate(self, data):
        accepted = False
        x = SchemaError([], [])
        for s in [Schema(s, error=self._error) for s in self._args]:
            try:
                validated = s.validate(data)
                if accepted:
                    raise SchemaError(['%r did not validate %r' % (self, data)],
                                      [self._error])
                accepted = True
            except SchemaError as _x:
                x = _x
        if not accepted:
            raise SchemaError(['%r did not validate %r' % (self, data)] + x.autos,
                              [self._error] + x.errors)
        return validated
```

If you are interested in this, I can open a PR.


Comments:

Patch:
diff --git a/README.rst b/README.rst
index 0d1855c..28186f8 100644
--- a/README.rst
+++ b/README.rst
@@ -298,6 +298,18 @@ for the same data:
     >>> Schema(And(Or(int, float), lambda x: x > 0)).validate(3.1415)
     3.1415
 
+In a dictionary, you can also combine two keys in a "one or the other" manner. To do
+so, use the `Or` class as a key:
+
+.. code:: python
+    from schema import Or, Schema
+    schema = Schema({
+        Or("key1", "key2", only_one=True): str
+    })
+
+    schema.validate({"key1": "test"}) # Ok
+    schema.validate({"key1": "test", "key2": "test"}) # SchemaWrongKeyError
+
 Hooks
 ~~~~~~~~~~
 You can define hooks which are functions that are executed whenever a valid key:value is found. 
diff --git a/schema.py b/schema.py
index 03e6909..cfae8b9 100644
--- a/schema.py
+++ b/schema.py
@@ -71,6 +71,7 @@ class And(object):
     """
     Utility function to combine validation directives in AND Boolean fashion.
     """
+
     def __init__(self, *args, **kw):
         self._args = args
         if not set(kw).issubset({'error', 'schema', 'ignore_extra_keys'}):
@@ -102,6 +103,15 @@ class And(object):
 class Or(And):
     """Utility function to combine validation directives in a OR Boolean
     fashion."""
+
+    def __init__(self, *args, **kwargs):
+        self.only_one = kwargs.pop('only_one', False)
+        self.reset()
+        super(Or, self).__init__(*args, **kwargs)
+
+    def reset(self):
+        self.got_one = False
+
     def validate(self, data):
         """
         Validate data using sub defined schema/expressions ensuring at least
@@ -114,7 +124,11 @@ class Or(And):
                                ignore_extra_keys=self._ignore_extra_keys)
                   for s in self._args]:
             try:
-                return s.validate(data)
+                validation = s.validate(data)
+                if self.got_one and self.only_one:
+                    break
+                self.got_one = True
+                return validation
             except SchemaError as _x:
                 autos, errors = _x.autos, _x.errors
         raise SchemaError(['%r did not validate %r' % (self, data)] + autos,
@@ -170,6 +184,7 @@ class Use(object):
     For more general use cases, you can use the Use class to transform
     the data while it is being validate.
     """
+
     def __init__(self, callable_, error=None):
         if not callable(callable_):
             raise TypeError('Expected a callable, not %r' % callable_)
@@ -217,6 +232,7 @@ class Schema(object):
     Entry point of the library, use this class to instantiate validation
     schema for the data that will be validated.
     """
+
     def __init__(self, schema, error=None, ignore_extra_keys=False):
         self._schema = schema
         self._error = error
@@ -266,6 +282,10 @@ class Schema(object):
             coverage = set()  # matched schema keys
             # for each key and value find a schema entry matching them, if any
             sorted_skeys = sorted(s, key=self._dict_key_priority)
+            for skey in sorted_skeys:
+                if isinstance(skey, Or):
+                    skey.reset()
+
             for key, value in data.items():
                 for skey in sorted_skeys:
                     svalue = s[skey]


Test patch:
diff --git a/test_schema.py b/test_schema.py
index 553e841..e230ba1 100644
--- a/test_schema.py
+++ b/test_schema.py
@@ -81,6 +81,16 @@ def test_or():
     with SE: Or().validate(2)
 
 
+def test_or_only_one():
+    schema = Schema({
+        Or("test1", "test2", only_one=True): str
+    })
+    assert schema.validate({"test1": "value"})
+    assert schema.validate({"test2": "other_value"})
+    with SE: schema.validate({"test1": "value", "test2": "other_value"})
+    with SE: schema.validate({"othertest": "value"})
+
+
 def test_test():
     def unique_list(_list):
         return len(_list) == len(set(_list))