draft form interface

This commit is contained in:
Daniil Fajnberg 2021-12-22 08:54:36 +01:00
parent 9dafe3da8f
commit 9dbe472bbf

119
src/yamlhttpforms/form.py Normal file
View File

@ -0,0 +1,119 @@
from types import SimpleNamespace
from pathlib import Path
from typing import Dict
from yaml import safe_load
class FormField:
def __init__(self, name: str, default: str = None, options: Dict[str, str] = None, hidden: bool = False):
self.name = name
self._default = default
self.options = options
self.hidden = hidden
def __repr__(self) -> str:
s = '<InputField' if self.options is None else '<SelectField'
s += f': name="{self.name}"'
if self.hidden:
s += ', hidden=True'
if self._default is not None:
s += f', default="{self.default}"'
return s + '>'
def __str__(self) -> str:
return repr(self)
@property
def default(self) -> str:
return self._default() if callable(self._default) else self._default
@property
def special(self) -> bool:
return self.hidden and self._default is None
def valid_option(self, option: str) -> bool:
return self.options is None or option in self.options.keys() or option in self.options.values()
def clean_value(self, value: str) -> str:
if self.options is None or value in self.options.keys():
return value
# Try to find an unambiguous match in the visible option texts:
key = None
for option_key, option_text in self.options.items():
if option_text == value:
if key is not None:
raise LookupError(f'More than one options key has the text "{value}" in {self}')
key = option_key
if key is not None:
return key
raise ValueError(f'"{value}" is not a valid option for {self}')
class Form(SimpleNamespace):
def __init__(self, definition: Dict[str, Dict], full_payload: bool = True):
"""
...
Args:
definition:
The field definition dictionary
full_payload (optional):
Defines the behavior during payload validation/creation.
If True (default), the fields' predefined default values will be inserted into the payload,
wherever no value (or `None`) is explicitly passed for that field.
If False, fields for which no values were explicitly passed, will not be included in the payload.
The exception is special fields (i.e. hidden fields with no default value);
these will cause an error, if no value is provided.
"""
self._full_payload_always = full_payload
for alias, field_def in definition.items():
setattr(self, alias, FormField(**field_def))
@property
def fields(self) -> Dict[str, FormField]:
return {k: v for k, v in self.__dict__.items() if isinstance(v, FormField)}
@property
def special_fields(self) -> Dict[str, FormField]:
return {k: v for k, v in self.__dict__.items() if isinstance(v, FormField) if v.special}
def get_payload(self, **kwargs) -> Dict[str, str]:
"""
Creates a request payload from the form's fields as a dictionary,
where the keys represent the name attributes of the form fields and the values the actual values to be passed.
This payload can then be used for example in a GET request in the form of url parameters (after encoding it
in the appropriate string form) or in a regular POST request.
Args:
kwargs (optional):
Every key must correspond to a key in the internal fields dictionary.
Values will be passed into the payload (if they pass validation).
Select fields with predefined options will only allow one of the options to be passed.
If `None` is passed as a value, the corresponding field's default value will be used in the payload.
Returns:
Validated name-value-mapping to be used for HTTP requests from the form's fields.
"""
payload = {}
for key, field in self.fields.items():
if key in kwargs.keys():
value = kwargs[key]
if value is None:
payload[field.name] = field.default
else:
payload[field.name] = field.clean_value(value)
elif field.special:
raise ValueError(f"`{key}` is a special field (hidden, no-defeault), but no argument was passed.")
elif self._full_payload_always:
payload[field.name] = field.default
return payload
def load_form(*def_paths: Path, full_payload: bool = True) -> Form:
definition = {}
for path in def_paths:
with open(path, 'r') as f:
definition.update(safe_load(f))
return Form(definition, full_payload=full_payload)