Add generic catch-all field for any data received from JSON (`taco-web`).

We need to account for the case when `taco-web` provides large numbers (bigints) as strings.
Add tests.
pull/3585/head
derekpierre 2025-03-20 10:27:59 -04:00
parent 1cb40795d0
commit 1b13c50b02
No known key found for this signature in database
4 changed files with 122 additions and 4 deletions

View File

@ -33,6 +33,7 @@ from nucypher.policy.conditions.exceptions import (
RPCExecutionFailed,
)
from nucypher.policy.conditions.lingo import (
AnyField,
ConditionType,
ExecutionCallAccessControlCondition,
ReturnValueTest,
@ -71,7 +72,7 @@ class RPCCall(ExecutionCall):
"null": "Undefined method name",
},
)
parameters = fields.List(fields.Field, required=False, allow_none=True)
parameters = fields.List(AnyField, required=False, allow_none=True)
@validates("method")
def validate_method(self, value):

View File

@ -17,6 +17,7 @@ from nucypher.policy.conditions.json.base import (
JsonRequestCall,
)
from nucypher.policy.conditions.lingo import (
AnyField,
ConditionType,
ExecutionCallAccessControlCondition,
ReturnValueTest,
@ -26,7 +27,7 @@ from nucypher.policy.conditions.lingo import (
class BaseJsonRPCCall(JsonRequestCall, ABC):
class Schema(JsonRequestCall.Schema):
method = fields.Str(required=True)
params = fields.Field(required=False, allow_none=True)
params = AnyField(required=False, allow_none=True)
query = JSONPathField(required=False, allow_none=True)
authorization_token = fields.Str(required=False, allow_none=True)

View File

@ -39,6 +39,39 @@ from nucypher.policy.conditions.types import ConditionDict, Lingo
from nucypher.policy.conditions.utils import CamelCaseSchema, ConditionProviderManager
class AnyField(fields.Field):
"""
Catch all field for all data types received in JSON.
However, `taco-web` will provide bigints as strings since typescript can't handle large
numbers as integers, so those need converting to integers.
"""
def _convert_any_large_integers_from_string(self, value):
if isinstance(value, list):
return [
self._convert_any_large_integers_from_string(item) for item in value
]
elif isinstance(value, dict):
return {
k: self._convert_any_large_integers_from_string(v)
for k, v in value.items()
}
elif isinstance(value, str):
try:
result = int(value)
return result
except ValueError:
# ignore
pass
return value
def _serialize(self, value, attr, obj, **kwargs):
return value
def _deserialize(self, value, attr, data, **kwargs):
return self._convert_any_large_integers_from_string(value)
class _ConditionField(fields.Dict):
"""Serializes/Deserializes Conditions to/from dictionaries"""
@ -511,7 +544,7 @@ class ReturnValueTest:
class ReturnValueTestSchema(CamelCaseSchema):
SKIP_VALUES = (None,)
comparator = fields.Str(required=True, validate=OneOf(_COMPARATOR_FUNCTIONS))
value = fields.Raw(
value = AnyField(
allow_none=False, required=True
) # any valid type (excludes None)
index = fields.Int(

View File

@ -1,4 +1,5 @@
import json
from collections import namedtuple
import pytest
from packaging.version import parse as parse_version
@ -9,7 +10,7 @@ from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
from nucypher.policy.conditions.exceptions import (
InvalidConditionLingo,
)
from nucypher.policy.conditions.lingo import ConditionLingo, ConditionType
from nucypher.policy.conditions.lingo import AnyField, ConditionLingo, ConditionType
from tests.constants import TESTERCHAIN_CHAIN_ID
@ -376,3 +377,85 @@ def test_lingo_data(conditions_test_data):
for name, condition_dict in conditions_test_data.items():
condition_class = ConditionLingo.resolve_condition_class(condition_dict)
_ = condition_class.from_dict(condition_dict)
@pytest.mark.parametrize(
"value",
[
1231323123132,
2121.23211,
False,
'"foo"', # string
":userAddress", # context variable
"0xaDD9D957170dF6F33982001E4c22eCCdd5539118", # string
"0x1234", # hex string
125, # int
-123456789, # negative int
1.223, # float
True, # bool
[1, 1.2314, False, "love"], # list of different types
["a", "b", "c"], # list
[True, False], # list of bools
{"name": "John", "age": 22}, # dict
namedtuple("MyStruct", ["field1", "field2"])(1, "a"),
[True, 2, 6.5, "0x123"],
],
)
def test_any_field_various_types(value):
field = AnyField()
deserialized_value = field._deserialize(value, attr=None, data=None)
serialized_value = field._serialize(deserialized_value, attr=None, obj=None)
assert deserialized_value == serialized_value
assert deserialized_value == value
@pytest.mark.parametrize(
"integer_value",
[
2**256 - 1, # uint256 max
-(2**255), # int256 min
123132312, # safe int
-1231231, # safe negative int
],
)
def test_any_field_integer_str_and_no_str_conversion(integer_value):
field = AnyField()
deserialized_raw_integer = field._deserialize(
value=integer_value, attr=None, data=None
)
deserialized_string_integer = field._deserialize(
value=str(integer_value), attr=None, data=None
)
assert deserialized_raw_integer == deserialized_string_integer
assert (
field._serialize(deserialized_raw_integer, attr=None, obj=None) == integer_value
)
assert (
field._serialize(deserialized_string_integer, attr=None, obj=None)
== integer_value
)
def test_any_field_nested_integer():
field = AnyField()
uint256_max = 2**256 - 1
int256_min = -(2**255)
regular_number = 12341231
parameters = [
f"{uint256_max}",
{"a": [f"{int256_min}", "my_string_value", "0xdeadbeef"], "b": regular_number},
]
# quoted numbers get unquoted after deserialization
expected_parameters = [
uint256_max,
{"a": [int256_min, "my_string_value", "0xdeadbeef"], "b": regular_number},
]
deserialized_parameters = field._deserialize(value=parameters, attr=None, data=None)
assert deserialized_parameters == expected_parameters