Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions pydecoder/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
'''Library fo json decode'''

from pyresult import (
ok,
error,
rmap,
fold
)
from toolz import curry, get_in

from pydecoder.primitives import ( # noqa pylint: disable=unused-import
to_int,
to_float,
to_string,
to_bool,
null,
)


def _to_key_list(keys):
if not isinstance(keys, tuple) and not isinstance(keys, list):
return (keys, )
return keys


@curry
def getter(json, keys): # pylint: disable=redefined-outer-name
'''Get data from json'''

try:
return ok(
get_in(
_to_key_list(keys),
json,
no_default=True
)
)
except KeyError:
return error(u'Value is empty or path/key {0!r} not found...'.format(keys))


@curry
def decode(creator, decoders, json_data):
'''Run decoders on json and result pass to creator function

json: (args -> value) -> List Decoder -> Json -> Result err value
'''
values = [decoder(getter(json_data)) for decoder in decoders] # pylint: disable=no-value-for-parameter

return rmap(creator, fold(values))
37 changes: 37 additions & 0 deletions pydecoder/operators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
'''Decoder operators'''

from pyresult import (
ok,
rmap,
and_then as andthen
)
from six.moves import reduce
from toolz import curry


@curry
def and_then(next_decoder, decoder, val):
'''Run `next_decoder` after `decoder` success run with `val`

and_then: (a -> Result b) -> (val -> Result a) -> val -> Result b
'''
return andthen(next_decoder, decoder(val))


@curry
def dmap(func, decoder, val):
'''Run `decoder` with `val` and then map `func` to result

dmap: (a -> value) -> (val -> Result a) -> val -> Result value
'''
return rmap(func, decoder(val))


@curry
def pipe(funcs, val):
'''Pass value trough funcs

pipe: List (value -> Result a) -> value -> Result a
'''
return reduce(lambda acc, item: andthen(item, acc), funcs, ok(val))
55 changes: 55 additions & 0 deletions pydecoder/primitives.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
'''Decode primitives'''

from pyresult import (
ok,
error
)
from six import u, string_types, text_type


def to_int(val):
'''Decode string/value to int'''
try:
return ok(int(val))
except (TypeError, ValueError) as err:
return error(text_type(err))


def to_float(val):
'''Decode string/value to float'''
try:
return ok(float(val))
except (TypeError, ValueError) as err:
return error(text_type(err))


def to_string(val):
'''Decode string/value to string'''
if val is None:
return error(u'Can\'t be null')

return ok(val) if isinstance(val, text_type) else ok(text_type(val, 'utf-8'))


def to_bool(val):
'''Decode string/value to boolean'''
if isinstance(val, string_types): # pylint: disable=no-else-return
lowered_string = val.lower()
if lowered_string == 'true': # pylint: disable=no-else-return
return ok(True)
elif lowered_string == 'false':
return ok(False)
else:
return error(u('String %s is invalid boolean value.' % val))
elif isinstance(val, bool):
return ok(val)
else:
return error(u('Value can\'t be decoded'))


def null(val):
'''Decode string/value to None'''
if val is not None and val.lower() not in ('none', 'null'):
return error(u('Value can\'t be decoded'))
return ok(None)
10 changes: 10 additions & 0 deletions pydecoder/structs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
'''Decoder structures'''

from toolz import curry


@curry
def array(factory, vals):
'''Decode string/value as list'''
return [factory(val) for val in vals]
103 changes: 16 additions & 87 deletions pydecoder/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

from collections import Iterator

from six import wraps, u, string_types, text_type
from six.moves import map, reduce
from six import wraps, u
from six.moves import map

from toolz import curry, first

Expand All @@ -13,7 +13,14 @@
error,
rmap,
fold,
and_then as andthen # pylint: disable=import-error
)

from pydecoder.primitives import (
to_int,
to_float,
to_string,
to_bool,
null,
)


Expand All @@ -38,7 +45,7 @@ def getter(tree, key):


@curry
def xml(creator, decoders, tree):
def decode(creator, decoders, tree):
'''Run decoders on xml and result pass to creator function

xml: (args -> value) -> List Decoder -> ElementTree -> Result value err
Expand All @@ -48,86 +55,8 @@ def xml(creator, decoders, tree):
return rmap(creator, fold(values))


@curry
def array(factory, vals):
'''Decode string/value as list'''
return [factory(val) for val in vals]


@curry
def and_then(next_decoder, decoder, val):
'''Run `next_decoder` after `decoder` success run with `val`

and_then: (a -> Result b) -> (val -> Result a) -> val -> Result b
'''
return andthen(next_decoder, decoder(val))


@curry
def dmap(func, decoder, val):
'''Run `decoder` with `val` and then map `func` to result

dmap: (a -> value) -> (val -> Result a) -> val -> Result value
'''
return rmap(func, decoder(val))


@curry
def pipe(funcs, val):
'''Pass value trough funcs

pipe: List (value -> Result a) -> value -> Result a
'''
return reduce(lambda acc, item: andthen(item, acc), funcs, ok(val))


@to_val
def to_int(val):
'''Decode string/value to int'''
try:
return ok(int(val))
except (TypeError, ValueError) as err:
return error(text_type(err))


@to_val
def to_float(val):
'''Decode string/value to float'''
try:
return ok(float(val))
except (TypeError, ValueError) as err:
return error(text_type(err))


@to_val
def to_string(val):
'''Decode string/value to string'''
if val is None:
return error(u'Can\'t be null')

return ok(val) if isinstance(val, text_type) else ok(text_type(val, 'utf-8'))


@to_val
def to_bool(val):
'''Decode string/value to boolean'''
if isinstance(val, string_types): # pylint: disable=no-else-return
lowered_string = val.lower()
if lowered_string == 'true': # pylint: disable=no-else-return
return ok(True)
elif lowered_string == 'false':
return ok(False)
else:
return error(u('String %s is invalid boolean value.' % val))
elif isinstance(val, bool):
return ok(val)
else:
return error(u('Value can\'t be decoded'))


@to_val
def null(val):
'''Decode string/value to None'''
if val is not None and val.lower() not in ('none', 'null'):
return error(u('Value can\'t be decoded'))
return ok(None)
to_int = to_val(to_int) # pylint: disable=invalid-name
to_float = to_val(to_float) # pylint: disable=invalid-name
to_string = to_val(to_string) # pylint: disable=invalid-name
to_bool = to_val(to_bool) # pylint: disable=invalid-name
null = to_val(null) # pylint: disable=invalid-name
111 changes: 111 additions & 0 deletions tests/test_decodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# pylint: disable=missing-docstring, import-error, redefined-outer-name, no-value-for-parameter

from json import loads
from xml.etree import ElementTree as ET

import pytest

from pydecoder.fields import required
from pydecoder import xml
from pydecoder import json
from pyresult import is_ok, is_error, value


PARAM = pytest.mark.parametrize


def xmlstring():
return '''
<b>
<a>1</a>
<a>2</a>
<a>3</a>
<c>foo</c>
<d>true</d>
</b>
'''


def tree():
return ET.fromstring(xmlstring())


def jsonstring():
return '{"a": [1, 2, 3], "c": "foo", "d": true}'


def json_data():
return loads(jsonstring())


DECODERS_AND_DATA = (
(xml, tree()),
(json, json_data()),
)


IDS = (
'XML',
'JSON',
)


def creator(values):
return values


@PARAM('decoder, data', DECODERS_AND_DATA, ids=IDS)
def test_return_ok_result(decoder, data):
rv = decoder.decode(
creator,
(
required('c', decoder.to_string),
),
data
)

assert is_ok(rv)
assert value(rv) == ['foo', ]


@PARAM('decoder, data', DECODERS_AND_DATA, ids=IDS)
def test_return_error_result(decoder, data):
rv = decoder.decode(
creator,
(
required('f', decoder.to_string),
),
data
)

assert is_error(rv)


@PARAM('decoder, data', DECODERS_AND_DATA, ids=IDS)
def test_return_error_result_with_aggregate_messages(decoder, data):
rv = decoder.decode(
creator,
(
required('f', decoder.to_string),
required('e', decoder.to_string),
),
data
)

assert is_error(rv)
for msg in rv.value:
assert msg.find(u'Value is empty') > -1


def test_json_decode_return_ok_result_by_path():
rv = json.decode(
creator,
(
required(['a', 2], json.to_int),
),
json_data()
)

assert is_ok(rv)
assert rv.value == [3, ]
Loading