From 9dbe472bbf306352b9cd74d6fdc96b1956df8f69 Mon Sep 17 00:00:00 2001 From: Daniil Fajnberg Date: Wed, 22 Dec 2021 08:54:36 +0100 Subject: [PATCH] draft form interface --- src/yamlhttpforms/form.py | 119 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/yamlhttpforms/form.py diff --git a/src/yamlhttpforms/form.py b/src/yamlhttpforms/form.py new file mode 100644 index 0000000..81d40a0 --- /dev/null +++ b/src/yamlhttpforms/form.py @@ -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 = '' + + 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)