Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streams support #768

Closed
wants to merge 10 commits into from
Closed

Streams support #768

wants to merge 10 commits into from

Conversation

stleary
Copy link
Owner

@stleary stleary commented Sep 4, 2023

What problem does this code solve?
JSONObject and JSONArray should support streams for functional programming.
There are multiple ways this could be added. If you know of a better way, please post your comments to this PR.

This change requires Java 8. Going forward, Java 6 and Java 7 library builds are no longer supported. The last release for Java 6/7 is 20230618. Users who still need Java 6/7 support should open an issue in this repo.

Risks
Low

Changes to the API?
New API methods were added

Will this require a new release?
Yes

Should the documentation be updated?
No

Does it break the unit tests?
No. New unit tests were added

Was any code refactored in this commit?

  • gradlew permissions were updated
  • ParserConfiguration warnings were addressed

Review status
code review action items pending

@johnjaylward
Copy link
Contributor

I'm trying to add review notes here, but it seems that GitHub is having issues.

I think for this implementation we should also consider #673 and #674.

Copy link
Contributor

@johnjaylward johnjaylward left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks like a good start, However I'd personally like to see a few more test cases around getting data out and null handling in both the JSONObject and JSONArray streams.

Looking at #673 and #674, I think we should look towards always returning valid JSON value types from the streams. It is something we missed out on with the iterators and prevents people from having a consistent interface.

We should also be sure that the streams themselves (specifically the ones provided as part of the API, not the follow-on calls as part of the tests) do not throw exceptions by using opt operations wherever possible.

* @return Stream of array elements
*/
public Stream<Object> stream() {
return IntStream.range(0, length()).mapToObj(this::get);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should not be this::get and instead should be this::opt. Stream should not throw exceptions, which get will do if a value is null.

We should also be sure to include at least one test case that has null in the array.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should not be this::get and instead should be this::opt. Stream should not throw exceptions, which get will do if a value is null.

We should also be sure to include at least one test case that has null in the array.

Seems to be working as is, but not sure why. Test cases are updated, let me know if this code or the tests should still be changed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stepped through the code and that Parser is converting null to JSONObject.NULL, which is expected.

Let's add these 2 additional tests, which are very similar, but add different ways of composing the data instead of just using the parser. This second test shows the issue I was worried about:

@Test
    public void testStream_ComposeFromArray() {
        @SuppressWarnings("boxing")
        Object[] expectedList = new Object[] { 1, 2, 3, true, false, null, "abc", "def", new JSONObject("{ \"a\": \"b\" }"), new JSONArray()};
        JSONArray jsonArray = new JSONArray(expectedList);
        
        List<Object> actualList = jsonArray.stream()
                .collect(Collectors.toList());
        System.out.println(actualList);
        assertEquals(actualList.size(), expectedList.length);
        for (int i = 0; i < actualList.size(); ++i) {
            Object actual = actualList.get(i);
            Object expected = expectedList[i];
            if (actual instanceof JSONObject) {
                assertTrue("Json Object test",((JSONObject) actual).similar(expected));
            } else if (actual instanceof JSONArray) {
                assertTrue("Json Array test", ((JSONArray) actual).similar(expected));
            } else if (JSONObject.NULL.equals(expected)) {
                assertTrue("Null test", JSONObject.NULL.equals(actual));
            } else {
                assertEquals("value test: "+expected, expected, actual);
            }
        }
    }
    @Test
    public void testStream_ComposeFromCode() {
        JSONArray jsonArray = new JSONArray();
        jsonArray.put(1).put(2).put(3)
            .put(true).put(false)
            .put((Object)null)
            .put("abc").put("def")
            .put(new JSONObject("{ \"a\": \"b\" }"))
            .put(new JSONArray())
        ;
        @SuppressWarnings("boxing")
        Object[] expectedList = new Object[] { 1, 2, 3, true, false, null, "abc", "def", new JSONObject("{ \"a\": \"b\" }"), new JSONArray()};
        
        List<Object> actualList = jsonArray.stream()
                .collect(Collectors.toList());
        System.out.println(actualList);
        assertEquals(actualList.size(), expectedList.length);
        for (int i = 0; i < actualList.size(); ++i) {
            Object actual = actualList.get(i);
            Object expected = expectedList[i];
            if (actual instanceof JSONObject) {
                assertTrue("Json Object test",((JSONObject) actual).similar(expected));
            } else if (actual instanceof JSONArray) {
                assertTrue("Json Array test", ((JSONArray) actual).similar(expected));
            } else if (JSONObject.NULL.equals(expected)) {
                assertTrue("Null test", JSONObject.NULL.equals(actual));
            } else {
                assertEquals("value test: "+expected, expected, actual);
            }
        }
    }

Copy link
Owner Author

@stleary stleary Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests.
Replaced this::get with this::opt

@@ -81,7 +74,13 @@
* @author JSON.org
* @version 2016-08-15
*/
public class JSONObject {
public class JSONObject implements Iterable<String> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the best Iterable<> that could have been chosen.

Would it make more sense to implement this closer to the Map<>.entrySet()? Something like: public class JSONObject implements Iterable<Map.Entry<String, Object>>?

Or maybe even better would be to keep it very similar to Map<> and not have it directly implement Iterable<> at all. Instead offer similar functions that the Map interface does:

  • keySet
  • entrySet
  • values

Each of those methods would return proper Iterable implementations.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation now streams entrySet() without Iterable<>.

JSONArray jsonArray = new JSONArray(data);

// Get employees from the engineering dept
List<String> actualList = StreamSupport.stream(jsonArray.spliterator(), false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't seem to be testing the new Stream method. It appears to only be testing Iteratable<>.spliterator() method.

This is a valid test, but I don't think this is doing the coverage that you wanted for the new method.

Copy link
Owner Author

@stleary stleary Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with new tests

@stleary
Copy link
Owner Author

stleary commented Sep 16, 2023

I'm trying to add review notes here, but it seems that GitHub is having issues.

I think for this implementation we should also consider #673 and #674.

Can this be done in a subsequent commit? The other issues seem out of scope for this initial implementation.

public void testStream_1_3_2() {
JSONArray jsonArray = null;

assertThrows(NullPointerException.class, () -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this test is adding anything here. It seems obvious that an NPE would be thrown in this case.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

public void test_2_3_2() {
JSONObject jsonObject = null;

assertThrows(NullPointerException.class, () -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this test is adding anything here. It seems obvious that an NPE would be thrown in this case.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

* @return Stream of array elements
*/
public Stream<Object> stream() {
return IntStream.range(0, length()).mapToObj(this::get);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stepped through the code and that Parser is converting null to JSONObject.NULL, which is expected.

Let's add these 2 additional tests, which are very similar, but add different ways of composing the data instead of just using the parser. This second test shows the issue I was worried about:

@Test
    public void testStream_ComposeFromArray() {
        @SuppressWarnings("boxing")
        Object[] expectedList = new Object[] { 1, 2, 3, true, false, null, "abc", "def", new JSONObject("{ \"a\": \"b\" }"), new JSONArray()};
        JSONArray jsonArray = new JSONArray(expectedList);
        
        List<Object> actualList = jsonArray.stream()
                .collect(Collectors.toList());
        System.out.println(actualList);
        assertEquals(actualList.size(), expectedList.length);
        for (int i = 0; i < actualList.size(); ++i) {
            Object actual = actualList.get(i);
            Object expected = expectedList[i];
            if (actual instanceof JSONObject) {
                assertTrue("Json Object test",((JSONObject) actual).similar(expected));
            } else if (actual instanceof JSONArray) {
                assertTrue("Json Array test", ((JSONArray) actual).similar(expected));
            } else if (JSONObject.NULL.equals(expected)) {
                assertTrue("Null test", JSONObject.NULL.equals(actual));
            } else {
                assertEquals("value test: "+expected, expected, actual);
            }
        }
    }
    @Test
    public void testStream_ComposeFromCode() {
        JSONArray jsonArray = new JSONArray();
        jsonArray.put(1).put(2).put(3)
            .put(true).put(false)
            .put((Object)null)
            .put("abc").put("def")
            .put(new JSONObject("{ \"a\": \"b\" }"))
            .put(new JSONArray())
        ;
        @SuppressWarnings("boxing")
        Object[] expectedList = new Object[] { 1, 2, 3, true, false, null, "abc", "def", new JSONObject("{ \"a\": \"b\" }"), new JSONArray()};
        
        List<Object> actualList = jsonArray.stream()
                .collect(Collectors.toList());
        System.out.println(actualList);
        assertEquals(actualList.size(), expectedList.length);
        for (int i = 0; i < actualList.size(); ++i) {
            Object actual = actualList.get(i);
            Object expected = expectedList[i];
            if (actual instanceof JSONObject) {
                assertTrue("Json Object test",((JSONObject) actual).similar(expected));
            } else if (actual instanceof JSONArray) {
                assertTrue("Json Array test", ((JSONArray) actual).similar(expected));
            } else if (JSONObject.NULL.equals(expected)) {
                assertTrue("Null test", JSONObject.NULL.equals(actual));
            } else {
                assertEquals("value test: "+expected, expected, actual);
            }
        }
    }

@stleary
Copy link
Owner Author

stleary commented Sep 20, 2023

Added the new unit tests

@stleary
Copy link
Owner Author

stleary commented Oct 5, 2023

Closing due to lack of activity.

@stleary stleary closed this Oct 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants