JsonPointerStartsWithTest.java

package tools.jackson.core.unittest.jsonptr;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonPointer;
import tools.jackson.core.JsonToken;
import tools.jackson.core.ObjectReadContext;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.unittest.*;

import static org.junit.jupiter.api.Assertions.*;

// Tests for [core#1637]: `JsonPointer.startsWith(JsonPointer)`
class JsonPointerStartsWithTest extends JacksonCoreTestBase
{
    private final JsonFactory JSON_F = new JsonFactory();

    @Test
    void startsWithEmpty() {
        JsonPointer ptr = JsonPointer.compile("/a/b/c");
        assertTrue(ptr.startsWith(JsonPointer.empty()), "Any pointer should start with the empty pointer");
        assertTrue(JsonPointer.empty().startsWith(JsonPointer.empty()), "Empty pointer should start with empty pointer");
    }

    @Test
    void startsWithNull() {
        JsonPointer ptr = JsonPointer.compile("/a/b/c");
        assertFalse(ptr.startsWith(null), "Should return false for null input");
    }

    @ParameterizedTest
    @CsvSource({
        "/a/b/c, /a",
        "/a/b/c, /a/b",
        "/a/b/c, /a/b/c",
        "/1/2/3, /1",
        "/1/2/3, /1/2",
        "/prop/0/leaf, /prop/0",
        "/~1slash/~0tilde, /~1slash"
    })
    void startsWithValidPrefix(String full, String prefix) {
        JsonPointer fullPtr = JsonPointer.compile(full);
        JsonPointer prefixPtr = JsonPointer.compile(prefix);
        assertTrue(fullPtr.startsWith(prefixPtr),
            String.format("Pointer '%s' should start with '%s'", full, prefix));
    }

    @ParameterizedTest
    @CsvSource({
        "/a/b/c, /b",
        "/a/b/c, /a/c",
        "/a/b/c, /a/b/c/d",
        "/1/2/3, /2",
        "/1/2/3, /1/3",
        "/1/2/3, /1/2/3/4",
        "/prop/0, /prop/1",
        "/a, /b",
        // Matching is by segment, not by raw String prefix:
        "/abc, /ab",
        "/abc/d, /ab",
        "/abc/d, /abc/d/e",
        "/12/3, /1"
    })
    void startsWithInvalidPrefix(String full, String prefix) {
        JsonPointer fullPtr = JsonPointer.compile(full);
        JsonPointer prefixPtr = JsonPointer.compile(prefix);
        assertFalse(fullPtr.startsWith(prefixPtr),
            String.format("Pointer '%s' should NOT start with '%s'", full, prefix));
    }

    @Test
    void startsWithEscaped() {
        JsonPointer fullPtr = JsonPointer.compile("/~1part1/~0part2/end");

        assertTrue(fullPtr.startsWith(JsonPointer.compile("/~1part1")));
        assertTrue(fullPtr.startsWith(JsonPointer.compile("/~1part1/~0part2")));

        // Mismatch in escaping
        assertFalse(fullPtr.startsWith(JsonPointer.compile("/part1")));
        // Escaped slash is part of one segment, not a segment separator
        assertFalse(fullPtr.startsWith(JsonPointer.compile("/")));
    }

    // Matching is on decoded segments, so equal-decoding pointers match even
    // when their String representations (and hence `equals()`) differ
    @Test
    void startsWithNotSameAsEquals() {
        JsonPointer valid = JsonPointer.compile("/a~0b");
        // "~b" is not a valid escape and is decoded as-is; same segment as above
        JsonPointer invalidEsc = JsonPointer.compile("/a~b");

        assertEquals("a~b", valid.getMatchingProperty());
        assertEquals("a~b", invalidEsc.getMatchingProperty());
        assertNotEquals(valid, invalidEsc);

        assertTrue(valid.startsWith(invalidEsc));
        assertTrue(invalidEsc.startsWith(valid));
    }

    @Test
    void startsWithTypeSafety() {
        // "/0" is index 0, "/00" is property name "00"
        JsonPointer indexPtr = JsonPointer.compile("/0/next");
        JsonPointer propPtr = JsonPointer.compile("/00/next");

        assertTrue(indexPtr.startsWith(JsonPointer.compile("/0")));
        assertFalse(indexPtr.startsWith(JsonPointer.compile("/00")));

        assertTrue(propPtr.startsWith(JsonPointer.compile("/00")));
        assertFalse(propPtr.startsWith(JsonPointer.compile("/0")));
    }

    @Test
    void startsWithLongerPrefix() {
        JsonPointer ptr = JsonPointer.compile("/a/b");
        JsonPointer longer = JsonPointer.compile("/a/b/c");
        assertFalse(ptr.startsWith(longer), "Pointer should not start with a longer pointer");
    }

    // [core#788]: empty String ("") is a valid property name, distinct from EMPTY pointer
    @Test
    void startsWithEmptyStringProperty() {
        // "/" is a single segment matching property with empty-String name
        JsonPointer emptyProp = JsonPointer.compile("/");
        JsonPointer emptyPropChild = JsonPointer.compile("//leaf");

        assertTrue(emptyProp.startsWith(JsonPointer.compile("/")));
        assertTrue(emptyPropChild.startsWith(JsonPointer.compile("/")));
        assertTrue(emptyPropChild.startsWith(JsonPointer.compile("//leaf")));

        // empty-String property "/" is NOT the same as the EMPTY (root) pointer as a prefix source:
        assertFalse(JsonPointer.empty().startsWith(emptyProp));
        // ...but every pointer (incl. "/") starts with the EMPTY pointer
        assertTrue(emptyProp.startsWith(JsonPointer.empty()));
    }

    // Pointers constructed by mutant factories build fresh segment chains: verify
    // those work as both receiver and argument
    @Test
    void startsWithDerivedPointers() {
        final JsonPointer full = JsonPointer.compile("/a/b/c/d");

        JsonPointer head = full.head(); // "/a/b/c"
        assertEquals("/a/b/c", head.toString());
        assertTrue(full.startsWith(head));
        assertTrue(head.startsWith(head.head()));
        assertFalse(head.startsWith(full));

        JsonPointer tail = full.tail(); // "/b/c/d"
        assertTrue(tail.startsWith(JsonPointer.compile("/b/c")));
        assertFalse(tail.startsWith(JsonPointer.compile("/a")));

        JsonPointer appended = JsonPointer.compile("/a/b").append(JsonPointer.compile("/c"));
        assertTrue(appended.startsWith(JsonPointer.compile("/a/b")));
        assertTrue(full.startsWith(appended));

        JsonPointer built = JsonPointer.compile("/a").appendIndex(3).appendProperty("x/y");
        assertTrue(built.startsWith(JsonPointer.compile("/a/3")));
        assertTrue(built.startsWith(JsonPointer.compile("/a").appendIndex(3)));
        assertTrue(built.startsWith(built));
        // "/03" is a property name, not index 3
        assertFalse(built.startsWith(JsonPointer.compile("/a/03")));
    }

    // Pointers from parsing context are built via `JsonPointer.forPath()`, another
    // separate construction path
    @Test
    void startsWithPointerFromContext() throws Exception
    {
        final String DOC = a2q("{'ob':{'array':[1,{'leaf':true}]}}");

        try (JsonParser p = JSON_F.createParser(ObjectReadContext.empty(), DOC)) {
            while (p.nextToken() != null) {
                if (p.currentToken() == JsonToken.VALUE_TRUE) {
                    break;
                }
            }
            JsonPointer ptr = p.streamReadContext().pathAsPointer();
            assertEquals("/ob/array/1/leaf", ptr.toString());

            assertTrue(ptr.startsWith(JsonPointer.empty()));
            assertTrue(ptr.startsWith(JsonPointer.compile("/ob")));
            assertTrue(ptr.startsWith(JsonPointer.compile("/ob/array/1")));
            assertTrue(ptr.startsWith(ptr));
            assertFalse(ptr.startsWith(JsonPointer.compile("/ob/array/0")));
            // and works as prefix argument as well:
            assertTrue(JsonPointer.compile("/ob/array/1/leaf/deeper").startsWith(ptr));
        }
    }
}