feat(gateway): safe type converter (1879 BUG-1/BUG-2)
This commit is contained in:
42
ingestor/converters.py
Normal file
42
ingestor/converters.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Safe coercion of JSON-transported values into typed OPC write values.
|
||||
|
||||
Fixes 1879 BUG-1 (bool('false') is True in Python) and BUG-2 (uncaught
|
||||
TypeError/ValueError from int/float on bad input).
|
||||
"""
|
||||
|
||||
_FALSE_STRINGS = {'false', '0', 'no', 'off', ''}
|
||||
|
||||
|
||||
class ConversionError(ValueError):
|
||||
"""Raised when a raw value cannot be coerced to the requested type."""
|
||||
|
||||
|
||||
def _to_bool(raw) -> bool:
|
||||
if isinstance(raw, bool):
|
||||
return raw
|
||||
if isinstance(raw, (int, float)):
|
||||
return bool(raw)
|
||||
return str(raw).strip().lower() not in _FALSE_STRINGS
|
||||
|
||||
|
||||
def to_typed_value(raw, data_type: str):
|
||||
"""Coerce raw (often a JSON string) to the given data_type.
|
||||
|
||||
data_type: 'boolean' | 'int' | 'float' | 'double' | 'string'.
|
||||
Raises ConversionError on any invalid input (never a bare TypeError).
|
||||
"""
|
||||
dtype = data_type.strip().lower()
|
||||
try:
|
||||
if dtype in ('bool', 'boolean'):
|
||||
return _to_bool(raw)
|
||||
if dtype in ('int', 'integer'):
|
||||
if isinstance(raw, str) and ('.' in raw or raw.strip() == ''):
|
||||
raise ConversionError(f'invalid int: {raw!r}')
|
||||
return int(raw)
|
||||
if dtype in ('float', 'double', 'real'):
|
||||
return float(raw)
|
||||
if dtype in ('str', 'string'):
|
||||
return str(raw)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ConversionError(f'cannot convert {raw!r} to {data_type}: {e}') from e
|
||||
raise ConversionError(f'unknown data_type: {data_type!r}')
|
||||
24
tests/unit/test_converters.py
Normal file
24
tests/unit/test_converters.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
from ingestor.converters import ConversionError, to_typed_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize('raw,expected', [
|
||||
('false', False), ('False', False), ('0', False), ('no', False),
|
||||
('off', False), ('', False), ('true', True), ('1', True), (True, True),
|
||||
])
|
||||
def test_bool_from_json_string(raw, expected):
|
||||
assert to_typed_value(raw, 'boolean') is expected
|
||||
|
||||
|
||||
def test_float_and_int():
|
||||
assert to_typed_value('42.5', 'float') == 42.5
|
||||
assert to_typed_value('7', 'int') == 7
|
||||
|
||||
|
||||
@pytest.mark.parametrize('raw,dtype', [
|
||||
(None, 'int'), ('abc', 'float'), ('123.123', 'int'), (None, 'float'),
|
||||
])
|
||||
def test_invalid_raises_conversion_error(raw, dtype):
|
||||
with pytest.raises(ConversionError):
|
||||
to_typed_value(raw, dtype)
|
||||
Reference in New Issue
Block a user