Skip to content

Commit

Permalink
feat: improved json validation using Results (#178)
Browse files Browse the repository at this point in the history
* feat: improved json validation using Results

* feat: add containsKey function to JsonObject
  • Loading branch information
vdrg authored Sep 5, 2023
1 parent 6a601ae commit 50b1d14
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 37 deletions.
29 changes: 27 additions & 2 deletions src/_modules/Json.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ struct JsonResult {
}

library JsonError {
bytes32 constant INVALID_JSON = keccak256("INVALID_JSON");
bytes32 constant IMMUTABLE_JSON = keccak256("IMMUTABLE_JSON");

function invalid() internal pure returns (JsonResult memory res) {
return JsonResult(Error(INVALID_JSON, "Invalid json").toResult());
}

function immutableJson() internal pure returns (JsonResult memory res) {
return JsonResult(Error(IMMUTABLE_JSON, "Json object is immutable").toResult());
}
Expand All @@ -42,6 +47,10 @@ library LibJsonResult {
function toError(JsonResult memory self) internal pure returns (Error memory) {
return self._inner.toError();
}

function toValue(JsonResult memory self) internal pure returns (JsonObject memory) {
return abi.decode(self._inner.toValue(), (JsonObject));
}
}

function Ok(JsonObject memory value) pure returns (JsonResult memory) {
Expand Down Expand Up @@ -70,6 +79,18 @@ library json {
return vulcan.hevm.parseJson(jsonObj.serialized);
}

function isValid(string memory jsonObj) internal pure returns (bool) {
try vulcan.hevm.parseJson(jsonObj) {
return true;
} catch {
return false;
}
}

function containsKey(JsonObject memory obj, string memory key) internal view returns (bool) {
return vulcan.hevm.keyExists(obj.serialized, key);
}

/// @dev Parses the value of the `key` contained on `jsonStr` as uint256.
/// @param obj The json object.
/// @param key The key from the `jsonStr` to parse.
Expand Down Expand Up @@ -191,8 +212,12 @@ library json {

/// @dev Creates a new JsonObject struct.
/// @return The JsonObject struct.
function create(string memory obj) internal pure returns (JsonObject memory) {
return JsonObject({id: "", serialized: obj});
function create(string memory obj) internal pure returns (JsonResult memory) {
if (!isValid(obj)) {
return JsonError.invalid();
}

return Ok(JsonObject({id: "", serialized: obj}));
}

/// @dev Serializes and sets the key and value for the provided json object.
Expand Down
51 changes: 33 additions & 18 deletions src/_modules/experimental/Request.sol
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,13 @@ library LibRequestBuilder {
}

function body(RequestBuilder memory self, string memory _body) internal pure returns (RequestBuilder memory) {
if (self.request.isOk()) {
Request memory req = self.request.toValue();
req.body = bytes(_body);
self.request = Ok(req);
if (self.request.isError()) {
return self;
}

Request memory req = self.request.toValue();
req.body = bytes(_body);
self.request = Ok(req);
return self;
}

Expand All @@ -215,26 +217,38 @@ library LibRequestBuilder {
pure
returns (RequestBuilder memory)
{
if (self.request.isOk()) {
Request memory req = self.request.toValue();
uint256 len = req.headers.length;
req.headers = new Header[](len + 1);
for (uint256 i; i < len; i++) {
req.headers[i] = req.headers[i];
}
req.headers[len] = Header({key: key, value: value});
self.request = Ok(req);
if (self.request.isError()) {
return self;
}

Request memory req = self.request.toValue();
uint256 len = req.headers.length;
req.headers = new Header[](len + 1);
for (uint256 i; i < len; i++) {
req.headers[i] = req.headers[i];
}
req.headers[len] = Header({key: key, value: value});
self.request = Ok(req);
return self;
}

function json(RequestBuilder memory self, JsonObject memory obj) internal pure returns (RequestBuilder memory) {
return self.json(obj.serialized);
// We assume the json has already been validated
return self.header("Content-Type", "application/json").body(obj.serialized);
}

function json(RequestBuilder memory self, string memory serialized) internal pure returns (RequestBuilder memory) {
// TODO: parse json and set error if it fails
return self.header("Content-Type", "application/json").body(serialized);
if (self.request.isError()) {
return self;
}

JsonResult memory res = jsonModule.create(serialized);
if (res.isError()) {
self.request = RequestResult(res._inner);
return self;
}

return self.json(res.toValue());
}
}

Expand Down Expand Up @@ -279,12 +293,13 @@ library LibRequest {
}

library LibResponse {
// TODO: validate response and return error if there are issues
function json(Response memory self) internal pure returns (JsonResult memory) {
return Ok(jsonModule.create(string(self.body)));
// create() will validate the json
return jsonModule.create(string(self.body));
}

function text(Response memory self) internal pure returns (StringResult memory) {
// TODO: maybe do some encoding validation? or check not empty?
return Ok(string(self.body));
}

Expand Down
55 changes: 38 additions & 17 deletions test/_modules/Json.t.sol
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.13 <0.9.0;

import {Test, expect, println, json, JsonObject, vulcan} from "../../src/test.sol";
Expand All @@ -10,7 +11,7 @@ contract JsonTest is Test {
}

function testParseImmutable() external {
Foo memory obj = abi.decode(json.create('{"foo":"bar"}').parse(), (Foo));
Foo memory obj = abi.decode(json.create('{"foo":"bar"}').unwrap().parse(), (Foo));
expect(obj.foo).toEqual("bar");
}

Expand All @@ -20,81 +21,101 @@ contract JsonTest is Test {
expect(obj.foo).toEqual("bar");
}

function testIsValid() external {
expect(json.isValid('{"foo":"bar"}')).toEqual(true);
expect(json.isValid("{}")).toEqual(true);
expect(json.isValid("[]")).toEqual(true);
expect(json.isValid('{"foo":"bar"')).toEqual(false);
expect(json.isValid('{"foo":bar"}')).toEqual(false);
expect(json.isValid("asdfasf")).toEqual(false);
}

function testContainsKey() external {
JsonObject memory obj = json.create('{"foo":"bar"}').unwrap();
expect(obj.containsKey(".foo")).toEqual(true);
expect(obj.containsKey(".bar")).toEqual(false);
}

function testGetMaxUint() external {
uint256 i = json.create('{"foo":"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}').unwrap()
.getUint(".foo");
expect(i).toEqual(type(uint256).max);
}

function testGetUint() external {
expect(json.create('{"foo":123}').getUint(".foo")).toEqual(123);
expect(json.create('{"foo":123}').unwrap().getUint(".foo")).toEqual(123);
}

function testGetUintArray() external {
uint256[] memory arr = json.create('{"foo":[123]}').getUintArray(".foo");
uint256[] memory arr = json.create('{"foo":[123]}').unwrap().getUintArray(".foo");
expect(arr.length).toEqual(1);
expect(arr[0]).toEqual(123);
}

function testGetInt() external {
expect(json.create('{"foo":-123}').getInt(".foo")).toEqual(-123);
expect(json.create('{"foo":-123}').unwrap().getInt(".foo")).toEqual(-123);
}

function testGetIntArray() external {
int256[] memory arr = json.create('{"foo":[-123]}').getIntArray(".foo");
int256[] memory arr = json.create('{"foo":[-123]}').unwrap().getIntArray(".foo");
expect(arr.length).toEqual(1);
expect(arr[0]).toEqual(-123);
}

function testGetBool() external {
expect(json.create('{"foo":true}').getBool(".foo")).toEqual(true);
expect(json.create('{"foo":true}').unwrap().getBool(".foo")).toEqual(true);
}

function testGetBoolArray() external {
bool[] memory arr = json.create('{"foo":[true]}').getBoolArray(".foo");
bool[] memory arr = json.create('{"foo":[true]}').unwrap().getBoolArray(".foo");
expect(arr.length).toEqual(1);
expect(arr[0]).toEqual(true);
}

function testGetAddress() external {
expect(json.create('{"foo":"0x0000000000000000000000000000000000000001"}').getAddress(".foo")).toEqual(
expect(json.create('{"foo":"0x0000000000000000000000000000000000000001"}').unwrap().getAddress(".foo")).toEqual(
address(1)
);
}

function testGetAddressArray() external {
address[] memory arr =
json.create('{"foo":["0x0000000000000000000000000000000000000001"]}').getAddressArray(".foo");
json.create('{"foo":["0x0000000000000000000000000000000000000001"]}').unwrap().getAddressArray(".foo");
expect(arr.length).toEqual(1);
expect(arr[0]).toEqual(address(1));
}

function testGetString() external {
expect(json.create('{"foo":"bar"}').getString(".foo")).toEqual("bar");
expect(json.create('{"foo":"bar"}').unwrap().getString(".foo")).toEqual("bar");
}

function testGetStringArray() external {
string[] memory arr = json.create('{"foo":["bar"]}').getStringArray(".foo");
string[] memory arr = json.create('{"foo":["bar"]}').unwrap().getStringArray(".foo");
expect(arr.length).toEqual(1);
expect(arr[0]).toEqual("bar");
}

function testGetBytes() external {
expect(json.create('{"foo":"0x1234"}').getBytes(".foo")).toEqual(hex"1234");
expect(json.create('{"foo":"0x1234"}').unwrap().getBytes(".foo")).toEqual(hex"1234");
}

function testGetBytesArray() external {
bytes[] memory arr = json.create('{"foo":["0x1234"]}').getBytesArray(".foo");
bytes[] memory arr = json.create('{"foo":["0x1234"]}').unwrap().getBytesArray(".foo");
expect(arr.length).toEqual(1);
expect(arr[0]).toEqual(hex"1234");
}

function testGetBytes32() external {
expect(
json.create('{"foo":"0x0000000000000000000000000000000000000000000000000000000000000001"}').getBytes32(
".foo"
)
json.create('{"foo":"0x0000000000000000000000000000000000000000000000000000000000000001"}').unwrap()
.getBytes32(".foo")
).toEqual(bytes32(uint256(1)));
}

function testGetBytes32Array() external {
bytes32[] memory arr = json.create(
'{"foo":["0x0000000000000000000000000000000000000000000000000000000000000001"]}'
).getBytes32Array(".foo");
).unwrap().getBytes32Array(".foo");
expect(arr.length).toEqual(1);
expect(arr[0]).toEqual(bytes32(uint256(1)));
}
Expand Down
20 changes: 20 additions & 0 deletions test/_modules/Request.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ contract RequestTest is Test {
expect(obj.getString(".authenticated")).toEqual("true");
}

function testJsonBody() external {
RequestClient memory client = request.create();

Response memory res = client.post("https://httpbin.org/post").json('{ "foo": "bar" }').send().unwrap();

expect(res.status).toEqual(200);

JsonObject memory obj = res.json().unwrap();

expect(obj.getString(".json.foo")).toEqual("bar");
}

function testJsonBodyFail() external {
RequestClient memory client = request.create();

ResponseResult memory res = client.post("https://httpbin.org/post").json('{ "foo": "bar" ').send();

expect(res.isError()).toEqual(true);
}

function testRequestFail() external {
Response memory res = request.get("https://httpbin.org/404").unwrap();
expect(res.status).toEqual(404);
Expand Down

0 comments on commit 50b1d14

Please sign in to comment.