inputs: regroup the encoding logic into the input handling objects
3 files changed, 75 insertions(+), 49 deletions(-)

M rework/helper.py
M rework/input.py
M tests/test_api.py
M rework/helper.py +8 -28
@@ 19,6 19,8 @@ from sqlhelp import select
 from inireader import reader
 from dateutil.parser import isoparse, parse as defaultparse
 
+from rework.input import inputio
+
 
 def utcnow():
     return datetime.utcnow().replace(tzinfo=pytz.utc)

          
@@ 415,34 417,12 @@ def pack_inputs(spec, args):
 
     raw = {}
     for field in spec:
-        name = field['name']
-        val = args.get(name)
-        if val is None:
-            if field['required']:
-                raise ValueError(
-                    f'missing required input: `{name}`'
-                )
-            continue
-        ftype = field['type']
-        if ftype == 'file':
-            assert isinstance(val, bytes)
-            raw[name] = val
-            continue
-        if ftype == 'string':
-            choices = field['choices']
-            if choices:
-                assert val in choices
-            raw[name] = val.encode('utf-8')
-            continue
-        if ftype == 'number':
-            raw[name] = str(val).encode('utf-8')
-            continue
-        if ftype == 'datetime':
-            if isinstance(val, str):
-                val = val.encode('utf-8')
-            else:
-                val = val.isoformat().encode('utf-8')
-            raw[name] = val
+        inp = inputio.from_type(
+            field['type'], field['name'], field['required'], field['choices']
+        )
+        val = inp.binary_encode(args)
+        if val is not None:
+            raw[inp.name] = val
 
     spec_keys = {field['name'] for field in spec}
     unknown_keys = set(args) - spec_keys

          
M rework/input.py +52 -19
@@ 1,9 1,14 @@ 
 import json
 
 
-class _base:
+class inputio:
     _fields = 'name', 'required', 'choices'
 
+    def __init__(self, name, required=False, choices=None):
+        self.name = name
+        self.required = required
+        self.choices = choices
+
     def __json_encode__(self):
         out = {
             name: getattr(self, name, None)

          
@@ 12,31 17,59 @@ class _base:
         out['type'] = self.__class__.__name__
         return out
 
-
-class number(_base):
+    @staticmethod
+    def from_type(atype, name, required, choices):
+        return globals()[atype](name, required, choices)
 
-    def __init__(self, name, required=False):
-        self.name = name
-        self.required = required
+    def val(self, args):
+        val = args.get(self.name)
+        if val is None:
+            if self.required:
+                raise ValueError(
+                    f'missing required input: `{self.name}`'
+                )
+        else:
+            if self.choices and val not in self.choices:
+                raise ValueError(
+                    f'{self.name} -> value not in {self.choices}'
+                )
+        return val
+
+
+class number(inputio):
+
+    def binary_encode(self, args):
+        val = self.val(args)
+        if val is not None:
+            return str(val).encode('utf-8')
 
 
-class string(_base):
+class string(inputio):
 
-    def __init__(self, name, required=False, choices=None):
-        self.name = name
-        self.required = required
-        self.choices = choices
+    def binary_encode(self, args):
+        val = self.val(args)
+        if val is not None:
+            return val.encode('utf-8')
 
 
-class file(_base):
+class file(inputio):
 
-    def __init__(self, name, required=False):
-        self.name = name
-        self.required = required
+    def binary_encode(self, args):
+        val = self.val(args)
+        if val is None:
+            return
+        assert isinstance(val, bytes) or val is None
+        return val
 
 
-class datetime(_base):
+class datetime(inputio):
 
-    def __init__(self, name, required=False):
-        self.name = name
-        self.required = required
+    def binary_encode(self, args):
+        val = self.val(args)
+        if val is None:
+            return
+        if isinstance(val, str):
+            val = val.encode('utf-8')
+        else:
+            val = val.isoformat().encode('utf-8')
+        return val

          
M tests/test_api.py +15 -2
@@ 143,7 143,18 @@ def test_with_inputs(engine, cleanup):
     with pytest.raises(ValueError) as err:
         api.schedule(
             engine, 'yummy',
+            {
+                'myfile.txt': b'something',
+                'option': 'quux'
+            }
+        )
+    assert err.value.args[0] == "option -> value not in ['foo', 'bar']"
+
+    with pytest.raises(ValueError) as err:
+        api.schedule(
+            engine, 'yummy',
             {'no-such-thing': 42,
+             'option': 'foo',
              'myfile.txt': b'something'
             }
         )

          
@@ 187,8 198,10 @@ def test_prepare_with_inputs(engine, cle
     with pytest.raises(ValueError) as err:
         api.prepare(
             engine, 'yummy',
-            inputdata={'no-such-thing': 42,
-             'myfile.txt': b'something'
+            inputdata={
+                'no-such-thing': 42,
+                'option': 'foo',
+                'myfile.txt': b'something'
             }
         )
     assert err.value.args[0] == 'unknown inputs: no-such-thing'