diff --git a/kaiba/casting.py b/kaiba/casting.py index 2b1a231..4fb311d 100644 --- a/kaiba/casting.py +++ b/kaiba/casting.py @@ -14,6 +14,16 @@ from kaiba.models.casting import CastToOptions +def unsafe_get_casting_function(cast_to: CastToOptions) -> Callable: + """Return casting function depending on name.""" + if cast_to == CastToOptions.INTEGER: + return CastToInteger() + + elif cast_to == CastToOptions.DECIMAL: + return CastToDecimal() + + return CastToDate() + @safe def get_casting_function(cast_to: CastToOptions) -> Callable: """Return casting function depending on name.""" diff --git a/kaiba/functions.py b/kaiba/functions.py index 46f7211..683535e 100644 --- a/kaiba/functions.py +++ b/kaiba/functions.py @@ -6,7 +6,7 @@ from returns.pointfree import bind from returns.result import Failure, ResultE, safe -from kaiba.casting import get_casting_function +from kaiba.casting import get_casting_function, unsafe_get_casting_function from kaiba.models.base import AnyType from kaiba.models.casting import Casting from kaiba.models.if_statement import Conditions, IfStatement @@ -168,6 +168,38 @@ def _apply_statement( return statement.otherwise or if_value +def unsafe_apply_separator( + mapped_values: List[AnyType], + separator: str, +) -> AnyType: + """Apply separator between the values of a List[Any]. + + :param mapped_values: The list of values to join with the separator + :type mapped_values: List[AnyType] + + :param separator: :term:`separator` value to join mapped_values list with + :type separator: str + + :return: Success/Failure containers + :rtype: AnyType + + Example + >>> from returns.pipeline import is_successful + >>> apply_separator(['a', 'b', 'c'], ' ').unwrap() + 'a b c' + >>> apply_separator([1, 'b', True], ' ').unwrap() + '1 b True' + >>> is_successful(apply_separator([], ' ')) + False + + """ + + if len(mapped_values) == 1: + return mapped_values[0] + + return separator.join([str(mapped) for mapped in mapped_values]) + + @safe def apply_separator( mapped_values: List[AnyType], @@ -342,6 +374,32 @@ def apply_regex( # noqa: WPS212, WPS234 return matches[num_group] +def unsafe_apply_casting( + value_to_cast: AnyType, + casting: Casting, +) -> AnyType: + """Casting one type of code to another. + + :param casting: :term:`casting` object + :type casting: dict + + :param value_to_cast: The value to cast to casting['to'] + :type value_to_cast: AnyType + + :return: Success/Failure containers + :rtype: AnyType + + Example + >>> apply_casting('123', Casting(**{'to': 'integer'})).unwrap() + 123 + >>> apply_casting('123.12', Casting(**{'to': 'decimal'})).unwrap() + Decimal('123.12') + """ + function = unsafe_get_casting_function(casting.to) + + return function(value_to_cast, casting.original_format).unwrap() + + def apply_casting( value_to_cast: Optional[AnyType], casting: Casting, diff --git a/kaiba/handlers.py b/kaiba/handlers.py index ddcb196..0db7747 100644 --- a/kaiba/handlers.py +++ b/kaiba/handlers.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Optional, Union from returns.curry import partial from returns.pipeline import flow, is_successful @@ -13,9 +13,11 @@ apply_regex, apply_separator, apply_slicing, + unsafe_apply_casting, unsafe_apply_default, unsafe_apply_if_statements, unsafe_apply_regex, + unsafe_apply_separator, ) from kaiba.models.attribute import Attribute from kaiba.models.base import AnyType @@ -73,6 +75,51 @@ def handle_data_fetcher( return Failure(ValueError('Failed to produce a value')) +def unsafe_handle_data_fetcher( + collection: Union[Dict[str, Any], List[Any]], + cfg: DataFetcher, +) -> Optional[AnyType]: + """Find a data at path or produce a value. + + return value can be: + - value found at path + - value found but sliced + - value found applied to regular expression + - conditional value depending on if statements + - default value if all the above still produces `None` + + Flow description: + + find data from path or None -> + apply regular expression -> + apply slicing -> + apply if statements -> + return default value if Failure else mapped value + """ + + produced_value = None + + produced_value = unsafe_fetch_data_by_keys( + collection, path=cfg.path, + ) + + if produced_value and cfg.regex: + produced_value = unsafe_apply_regex(produced_value, regex=cfg.regex) + + if produced_value and cfg.slicing: + produced_value = apply_slicing(produced_value, cfg.slicing) + + produced_value = unsafe_apply_if_statements( + produced_value, + cfg.if_statements, + ) + + return unsafe_apply_default( + produced_value, + cfg.default, + ) + + def handle_attribute( collection: Union[Dict[str, Any], List[Any]], cfg: Attribute, @@ -90,29 +137,33 @@ def handle_attribute( Return Result """ + fetched_values = [ - fetched.unwrap() - for fetched in # noqa: WPS361 - [ - handle_data_fetcher(collection, data_fetcher) + fetched + for fetched in [ # noqa: WPS361 + unsafe_handle_data_fetcher(collection, data_fetcher) for data_fetcher in cfg.data_fetchers ] - if is_successful(fetched) + if fetched is not None ] - # partially declare if statement and casting functions - ifs = partial(apply_if_statements, statements=cfg.if_statements) - - cast = safe(lambda the_value: the_value) - if cfg.casting: - cast = partial(apply_casting, casting=cfg.casting) - - return flow( - apply_separator(fetched_values, separator=cfg.separator), - fix(lambda _: None), # type: ignore - bind(ifs), - bind(cast), - rescue( - lambda _: apply_default(default=cfg.default), - ), - ) + attribute = None + + if fetched_values: + attribute = unsafe_apply_separator( + fetched_values, + separator=cfg.separator, + ) + + attribute = unsafe_apply_if_statements(attribute, cfg.if_statements) + + if attribute and cfg.casting: + attribute = unsafe_apply_casting(attribute, cfg.casting) + + attribute = unsafe_apply_default(attribute, default=cfg.default) + + # just return the modals so tests will still work. + if attribute: + return Success(attribute) + + return Failure(ValueError('Failed to produce a value'))