diff --git a/lib/src/main/java/io/ably/lib/object/ObjectType.java b/lib/src/main/java/io/ably/lib/object/ObjectType.java new file mode 100644 index 000000000..bef18ae95 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/ObjectType.java @@ -0,0 +1,13 @@ +package io.ably.lib.object; + +public enum ObjectType { + STRING, + NUMBER, + BOOLEAN, + BINARY, + JSON_OBJECT, + JSON_ARRAY, + LIVE_MAP, + LIVE_COUNTER, + UNKNOWN, +} diff --git a/lib/src/main/java/io/ably/lib/object/instance/LiveObjectInstance.java b/lib/src/main/java/io/ably/lib/object/instance/LiveObjectInstance.java new file mode 100644 index 000000000..f5bbfbb90 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/instance/LiveObjectInstance.java @@ -0,0 +1,193 @@ +package io.ably.lib.object.instance; + +import com.google.gson.JsonElement; +import io.ably.lib.object.ObjectType; +import io.ably.lib.object.instance.types.BinaryInstance; +import io.ably.lib.object.instance.types.BooleanInstance; +import io.ably.lib.object.instance.types.JsonArrayInstance; +import io.ably.lib.object.instance.types.JsonObjectInstance; +import io.ably.lib.object.instance.types.LiveCounterInstance; +import io.ably.lib.object.instance.types.LiveMapInstance; +import io.ably.lib.object.instance.types.NumberInstance; +import io.ably.lib.object.instance.types.StringInstance; +import io.ably.lib.objects.ObjectsSubscription; +import org.jetbrains.annotations.NonBlocking; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A direct-reference view of a single LiveObject (a {@code LiveMap} or {@code LiveCounter}) + * or a primitive value. Unlike {@code PathObject}, which resolves a path lazily against + * the LiveObjects graph at every call, an {@code Instance} is bound to a specific + * underlying value identified by its object id (for live objects) and dereferenced in + * O(1). + * + *

Java exposes type-specific sub-types ({@link LiveMapInstance}, + * {@link LiveCounterInstance}, and the primitive {@code *Instance} types). Use the + * {@code as*} helpers to obtain a sub-type wrapper without performing type validation. + * + *

Spec: RTINS1 + */ +public interface LiveObjectInstance { + + /** + * Returns the {@link ObjectType} of the value wrapped by this instance. Use this + * instead of dedicated {@code isLiveMap}/{@code isLiveCounter}/etc. checks. + * + * @return the wrapped object type + */ + @NotNull ObjectType getType(); + + /** + * Returns the object id of the wrapped LiveObject, or {@code null} when the wrapped + * value is a primitive. Only {@link LiveMapInstance} and {@link LiveCounterInstance} + * ever return a non-null id. + * + *

Spec: RTINS3 + * + * @return the wrapped object's id, or {@code null} for primitive instances + */ + @Nullable String getId(); + + /** + * Returns a JSON-serializable, recursively compacted snapshot of the wrapped value. + * Behaves identically to {@code PathObject#compactJson} except that it operates on + * the wrapped value directly instead of resolving a path. An {@code Instance} is + * always bound to a resolved value, so this always returns a non-null result; + * failures of the access API preconditions are signalled via {@code AblyException}. + * + *

Spec: RTINS11 + * + * @return the compacted JSON snapshot + */ + @NotNull JsonElement compactJson(); + + /** + * Subscribes a listener for updates on the underlying LiveObject. The listener is + * invoked whenever the wrapped object is changed by a local or remote operation. + * + *

Subscribe is not supported on primitive instances; implementations may throw + * when called on {@link NumberInstance}, {@link StringInstance}, + * {@link BooleanInstance}, {@link BinaryInstance}, {@link JsonObjectInstance} or + * {@link JsonArrayInstance}. + * + *

Spec: RTINS16 + * + * @param listener the listener to invoke on updates + * @return a subscription handle that can be used to unsubscribe this listener + */ + @NonBlocking + @NotNull ObjectsSubscription subscribe(@NotNull Listener listener); + + /** + * Unsubscribes the specified listener previously registered via + * {@link #subscribe(Listener)}. No-op if the listener is not currently subscribed. + * + * @param listener the listener to remove + */ + @NonBlocking + void unsubscribe(@NotNull Listener listener); + + /** + * Removes all listeners previously registered on this instance. + */ + @NonBlocking + void unsubscribeAll(); + + /** + * Returns this instance wrapped as a {@link LiveMapInstance}. + * + *

Best-effort cast; does not validate the underlying type. Read operations on + * the returned wrapper are always permitted; write/terminal operations will fail + * at call time if the wrapped value is not a {@code LiveMap}. + * + * @return a {@link LiveMapInstance} view of this instance + */ + @NotNull LiveMapInstance asLiveMap(); + + /** + * Returns this instance wrapped as a {@link LiveCounterInstance}. + * Best-effort cast; does not validate the underlying type. + * + * @return a {@link LiveCounterInstance} view of this instance + */ + @NotNull LiveCounterInstance asLiveCounter(); + + /** + * Returns this instance wrapped as a {@link NumberInstance}. + * Best-effort cast; does not validate the underlying type. + * + * @return a {@link NumberInstance} view of this instance + */ + @NotNull NumberInstance asNumber(); + + /** + * Returns this instance wrapped as a {@link StringInstance}. + * Best-effort cast; does not validate the underlying type. + * + * @return a {@link StringInstance} view of this instance + */ + @NotNull StringInstance asString(); + + /** + * Returns this instance wrapped as a {@link BooleanInstance}. + * Best-effort cast; does not validate the underlying type. + * + * @return a {@link BooleanInstance} view of this instance + */ + @NotNull BooleanInstance asBoolean(); + + /** + * Returns this instance wrapped as a {@link BinaryInstance}. + * Best-effort cast; does not validate the underlying type. + * + * @return a {@link BinaryInstance} view of this instance + */ + @NotNull BinaryInstance asBinary(); + + /** + * Returns this instance wrapped as a {@link JsonObjectInstance}. + * Best-effort cast; does not validate the underlying type. + * + * @return a {@link JsonObjectInstance} view of this instance + */ + @NotNull JsonObjectInstance asJsonObject(); + + /** + * Returns this instance wrapped as a {@link JsonArrayInstance}. + * Best-effort cast; does not validate the underlying type. + * + * @return a {@link JsonArrayInstance} view of this instance + */ + @NotNull JsonArrayInstance asJsonArray(); + + /** + * Listener interface for {@link LiveObjectInstance#subscribe(Listener) instance + * subscriptions}. + * + *

Spec: RTINS16a1 + */ + interface Listener { + /** + * Invoked when the wrapped LiveObject is modified. + * + * @param event the event describing the change + */ + void onUpdated(@NotNull SubscriptionEvent event); + } + + /** + * Event delivered to {@link Listener#onUpdated(SubscriptionEvent)} when the wrapped + * LiveObject is updated. + * + *

Spec: RTINS16e + */ + interface SubscriptionEvent { + /** + * Returns the {@link LiveObjectInstance} that was updated. + * + * @return the updated instance + */ + @NotNull LiveObjectInstance getInstance(); + } +} diff --git a/lib/src/main/java/io/ably/lib/object/instance/types/BinaryInstance.java b/lib/src/main/java/io/ably/lib/object/instance/types/BinaryInstance.java new file mode 100644 index 000000000..d0ef51a26 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/instance/types/BinaryInstance.java @@ -0,0 +1,23 @@ +package io.ably.lib.object.instance.types; + +import io.ably.lib.object.instance.LiveObjectInstance; +import org.jetbrains.annotations.NotNull; + +/** + * A read-only {@link LiveObjectInstance} bound to a binary primitive value + * (a {@code byte[]}). + * + *

{@link #getId()} always returns {@code null} for primitive instances, and + * subscribe operations are not supported. + */ +public interface BinaryInstance extends LiveObjectInstance { + + /** + * Returns the wrapped binary value. + * + *

Spec: RTINS4 + * + * @return the wrapped bytes + */ + byte @NotNull [] value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/instance/types/BooleanInstance.java b/lib/src/main/java/io/ably/lib/object/instance/types/BooleanInstance.java new file mode 100644 index 000000000..90c2ec3f8 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/instance/types/BooleanInstance.java @@ -0,0 +1,23 @@ +package io.ably.lib.object.instance.types; + +import io.ably.lib.object.instance.LiveObjectInstance; +import org.jetbrains.annotations.NotNull; + +/** + * A read-only {@link LiveObjectInstance} bound to a {@code Boolean} primitive value. + * + *

{@link #getId()} always returns {@code null} for primitive instances, and + * subscribe operations are not supported. + */ +public interface BooleanInstance extends LiveObjectInstance { + + /** + * Returns the wrapped boolean. + * + *

Spec: RTINS4 + * + * @return the wrapped boolean value + */ + @NotNull + Boolean value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/instance/types/JsonArrayInstance.java b/lib/src/main/java/io/ably/lib/object/instance/types/JsonArrayInstance.java new file mode 100644 index 000000000..fe5c5b99b --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/instance/types/JsonArrayInstance.java @@ -0,0 +1,24 @@ +package io.ably.lib.object.instance.types; + +import com.google.gson.JsonArray; +import io.ably.lib.object.instance.LiveObjectInstance; +import org.jetbrains.annotations.NotNull; + +/** + * A read-only {@link LiveObjectInstance} bound to a {@link JsonArray} primitive value. + * + *

{@link #getId()} always returns {@code null} for primitive instances, and + * subscribe operations are not supported. + */ +public interface JsonArrayInstance extends LiveObjectInstance { + + /** + * Returns the wrapped JSON array. + * + *

Spec: RTINS4 + * + * @return the wrapped JsonArray value + */ + @NotNull + JsonArray value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/instance/types/JsonObjectInstance.java b/lib/src/main/java/io/ably/lib/object/instance/types/JsonObjectInstance.java new file mode 100644 index 000000000..7a8c0bb4e --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/instance/types/JsonObjectInstance.java @@ -0,0 +1,24 @@ +package io.ably.lib.object.instance.types; + +import com.google.gson.JsonObject; +import io.ably.lib.object.instance.LiveObjectInstance; +import org.jetbrains.annotations.NotNull; + +/** + * A read-only {@link LiveObjectInstance} bound to a {@link JsonObject} primitive value. + * + *

{@link #getId()} always returns {@code null} for primitive instances, and + * subscribe operations are not supported. + */ +public interface JsonObjectInstance extends LiveObjectInstance { + + /** + * Returns the wrapped JSON object. + * + *

Spec: RTINS4 + * + * @return the wrapped JsonObject value + */ + @NotNull + JsonObject value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/instance/types/LiveCounterInstance.java b/lib/src/main/java/io/ably/lib/object/instance/types/LiveCounterInstance.java new file mode 100644 index 000000000..a05d4f15b --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/instance/types/LiveCounterInstance.java @@ -0,0 +1,72 @@ +package io.ably.lib.object.instance.types; + +import io.ably.lib.object.instance.LiveObjectInstance; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +/** + * A {@link LiveObjectInstance} bound to a {@code LiveCounter}. Provides type-safe + * access to counter operations such as {@link #value()}, {@link #increment(Number)} + * and {@link #decrement(Number)}. + */ +public interface LiveCounterInstance extends LiveObjectInstance { + + /** + * Returns the current value of the wrapped {@code LiveCounter}. + * + *

Spec: RTINS4 / RTLC5 + * + * @return the counter value + */ + @NotNull + Double value(); + + /** + * Increments the wrapped {@code LiveCounter} by {@code 1}. Equivalent to + * calling {@link #increment(Number)} with {@code 1}. + * + *

Spec: RTINS14a1 (default {@code amount} of {@code 1}) + * + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture increment(); + + /** + * Increments the wrapped {@code LiveCounter} by {@code amount}. + * + *

Sends a {@code COUNTER_INC} operation to the realtime system; the local state + * is updated when the operation is echoed back. + * + *

Spec: RTINS14 + * + * @param amount the amount to add (may be negative) + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture increment(@NotNull Number amount); + + /** + * Decrements the wrapped {@code LiveCounter} by {@code 1}. Equivalent to + * calling {@link #decrement(Number)} with {@code 1}. + * + *

Spec: RTINS15a1 (default {@code amount} of {@code 1}) + * + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture decrement(); + + /** + * Decrements the wrapped {@code LiveCounter} by {@code amount}. Equivalent to + * calling {@link #increment(Number)} with a negated value. + * + *

Spec: RTINS15 + * + * @param amount the amount to subtract (may be negative) + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture decrement(@NotNull Number amount); +} diff --git a/lib/src/main/java/io/ably/lib/object/instance/types/LiveMapInstance.java b/lib/src/main/java/io/ably/lib/object/instance/types/LiveMapInstance.java new file mode 100644 index 000000000..93ef30182 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/instance/types/LiveMapInstance.java @@ -0,0 +1,105 @@ +package io.ably.lib.object.instance.types; + +import io.ably.lib.object.instance.LiveObjectInstance; +import io.ably.lib.objects.type.map.LiveMapValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * A {@link LiveObjectInstance} bound to a {@code LiveMap}. Provides type-safe access to + * map-specific operations such as {@link #get(String)}, {@link #entries()} and + * {@link #set(String, LiveMapValue)}. + * + *

Operations are bound to the specific underlying {@code LiveMap}, dereferenced in + * O(1), and do not perform any path resolution. + */ +public interface LiveMapInstance extends LiveObjectInstance { + + /** + * Returns a {@link LiveObjectInstance} wrapping the value at {@code key} of the + * wrapped {@code LiveMap}, or {@code null} when the key is absent / tombstoned. + * + *

Spec: RTINS5 + * + * @param key the key to look up + * @return an instance wrapping the value at {@code key}, or {@code null} + */ + @Nullable + LiveObjectInstance get(@NotNull String key); + + /** + * Returns the entries (key, child {@link LiveObjectInstance}) of the wrapped + * {@code LiveMap}. + * + *

Spec: RTINS6 + * + * @return an unmodifiable iterable of entries + */ + @NotNull + @Unmodifiable + Iterable> entries(); + + /** + * Returns the keys of the wrapped {@code LiveMap}. + * + *

Spec: RTINS7 + * + * @return an unmodifiable iterable of keys + */ + @NotNull + @Unmodifiable + Iterable keys(); + + /** + * Returns the child {@link LiveObjectInstance}s for each value in the wrapped + * {@code LiveMap}. + * + *

Spec: RTINS8 + * + * @return an unmodifiable iterable of value instances + */ + @NotNull + @Unmodifiable + Iterable values(); + + /** + * Returns the number of (non-tombstoned) entries in the wrapped {@code LiveMap}. + * + *

Spec: RTINS9 + * + * @return the map size + */ + @NotNull + Long size(); + + /** + * Sets a key on the wrapped {@code LiveMap} to the provided value. Sends a + * {@code MAP_SET} operation to the realtime system; the local state is updated when + * the operation is echoed back. + * + *

Spec: RTINS12 + * + * @param key the key to set + * @param value the value to associate with {@code key} + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture set(@NotNull String key, @NotNull LiveMapValue value); + + /** + * Removes a key from the wrapped {@code LiveMap}. Sends a {@code MAP_REMOVE} + * operation to the realtime system; the local state is updated when the operation + * is echoed back. + * + *

Spec: RTINS13 + * + * @param key the key to remove + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture remove(@NotNull String key); +} diff --git a/lib/src/main/java/io/ably/lib/object/instance/types/NumberInstance.java b/lib/src/main/java/io/ably/lib/object/instance/types/NumberInstance.java new file mode 100644 index 000000000..a778000cf --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/instance/types/NumberInstance.java @@ -0,0 +1,23 @@ +package io.ably.lib.object.instance.types; + +import io.ably.lib.object.instance.LiveObjectInstance; +import org.jetbrains.annotations.NotNull; + +/** + * A read-only {@link LiveObjectInstance} bound to a {@code Number} primitive value. + * + *

{@link #getId()} always returns {@code null} for primitive instances, and + * subscribe operations are not supported. + */ +public interface NumberInstance extends LiveObjectInstance { + + /** + * Returns the wrapped number. + * + *

Spec: RTINS4 + * + * @return the wrapped numeric value + */ + @NotNull + Number value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/instance/types/StringInstance.java b/lib/src/main/java/io/ably/lib/object/instance/types/StringInstance.java new file mode 100644 index 000000000..9639adfda --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/instance/types/StringInstance.java @@ -0,0 +1,23 @@ +package io.ably.lib.object.instance.types; + +import io.ably.lib.object.instance.LiveObjectInstance; +import org.jetbrains.annotations.NotNull; + +/** + * A read-only {@link LiveObjectInstance} bound to a {@code String} primitive value. + * + *

{@link #getId()} always returns {@code null} for primitive instances, and + * subscribe operations are not supported. + */ +public interface StringInstance extends LiveObjectInstance { + + /** + * Returns the wrapped string. + * + *

Spec: RTINS4 + * + * @return the wrapped string value + */ + @NotNull + String value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/path/PathObject.java b/lib/src/main/java/io/ably/lib/object/path/PathObject.java new file mode 100644 index 000000000..0a2aaefd0 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/path/PathObject.java @@ -0,0 +1,294 @@ +package io.ably.lib.object.path; + +import com.google.gson.JsonElement; +import io.ably.lib.object.ObjectType; +import io.ably.lib.object.instance.LiveObjectInstance; +import io.ably.lib.object.path.types.BinaryPathObject; +import io.ably.lib.object.path.types.BooleanPathObject; +import io.ably.lib.object.path.types.JsonArrayPathObject; +import io.ably.lib.object.path.types.JsonObjectPathObject; +import io.ably.lib.object.path.types.LiveCounterPathObject; +import io.ably.lib.object.path.types.LiveMapPathObject; +import io.ably.lib.object.path.types.NumberPathObject; +import io.ably.lib.object.path.types.StringPathObject; +import io.ably.lib.objects.ObjectsSubscription; +import org.jetbrains.annotations.NonBlocking; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Provides a path-based, navigational view over the LiveObjects graph rooted at the + * channel's root {@code LiveMap}. A {@code PathObject} encapsulates a path expressed as + * an ordered list of string segments and resolves the path lazily against the current + * client-side state of the graph when read or write operations are invoked. + * + *

Resolution is best-effort: it observes the local object tree at the time the + * operation is called. There is no global transaction primitive, so the value at a given + * path can change between two calls on the same {@code PathObject} (e.g. between + * {@link #exists()} and a subsequent write) as updates from other clients are applied. + * + *

For the strongly-typed flavour of the API in Java, callers normally interact with + * type-specific sub-types ({@link LiveMapPathObject}, {@link LiveCounterPathObject}, and + * the primitive {@code *PathObject} types). Use the {@code as*} helpers to obtain a + * sub-type wrapper without performing type validation. + * + *

Spec: RTPO1, RTPO2 + */ +public interface PathObject { + + /** + * Returns the {@link ObjectType} of the value the resolved at this path currently. + * Use this instead of dedicated {@code isLiveMap}/{@code isLiveCounter}/etc. checks. + * + * @return the resolved object type at this path + */ + @NotNull ObjectType getType(); + + /** + * Returns a dot-delimited string representation of the stored path segments. + * Dot characters inside individual segments are escaped with a backslash, so a + * path with segments {@code ["a", "b.c", "d"]} is represented as {@code "a.b\.c.d"}. + * An empty path (i.e. the root {@code PathObject}) returns the empty string. + * + *

Spec: RTPO4 + * + * @return the dot-delimited path from the root to this position + */ + @NotNull String path(); + + /** + * Returns a new {@code PathObject} whose path is this path with the segments parsed + * from {@code path} appended. The {@code path} argument is a dot-delimited string; + * a backslash-escaped dot ({@code \.}) is treated as a literal dot within a segment. + * + *

This is purely navigational - no resolution against the LiveObjects graph is + * performed by this call. {@code pathObject.at("a.b.c")} is equivalent to + * {@code pathObject.get("a").get("b").get("c")} on a {@link LiveMapPathObject}. + * + *

For primitive {@code *PathObject} sub-types and {@link LiveCounterPathObject}, + * deeper navigation is not meaningful; implementations may throw or return a + * {@code PathObject} that will fail to resolve at read/write time. + * + *

Spec: RTPO6 + * + * @param path dot-delimited path to append to this path + * @return a new {@code PathObject} representing the deeper path + */ + @NotNull PathObject at(@NotNull String path); + + /** + * Resolves this path and returns a {@link LiveObjectInstance} wrapping the underlying + * value if it is a {@code LiveMap} or {@code LiveCounter}. + * + *

Returns {@code null} when the resolved value is a primitive (LiveObjects with + * no object id), when the path does not resolve, or when called on primitive + * {@code *PathObject} sub-types. + * + *

Spec: RTPO8 + * + * @return a {@link LiveObjectInstance} wrapping the resolved live object, or {@code null} + */ + @Nullable LiveObjectInstance instance(); + + /** + * Returns a JSON-serializable, recursively compacted snapshot of the value at this + * path. Behaves like the spec's {@code compact} except that {@code Binary} values + * are base64-encoded and cyclic references are represented as + * {@code { "objectId": ... }} markers, so the result is safe to serialise as JSON. + * + *

Returns {@code null} when the path does not resolve. + * + *

Spec: RTPO14 + * + * @return the compacted JSON snapshot, or {@code null} if the path does not resolve + */ + @Nullable JsonElement compactJson(); + + /** + * Subscribes a listener for path-based update events. The listener is invoked when + * an operation modifies the value at this path. The same path may be subscribed by + * multiple listeners independently. + * + *

Spec: RTPO19 + * + * @param listener the listener to invoke on updates + * @return a subscription handle that can be used to unsubscribe this listener + */ + @NonBlocking + @NotNull ObjectsSubscription subscribe(@NotNull Listener listener); + + /** + * Subscribes a listener for path-based update events using the provided + * {@link SubscriptionOptions}. Options control coverage rules such as the + * {@code depth} of nested updates that trigger the listener. + * + *

Spec: RTPO19 + * + * @param listener the listener to invoke on updates + * @param options optional subscription options, may be {@code null} + * @return a subscription handle that can be used to unsubscribe this listener + */ + @NonBlocking + @NotNull ObjectsSubscription subscribe(@NotNull Listener listener, @Nullable SubscriptionOptions options); + + /** + * Unsubscribes the specified listener previously registered via + * {@link #subscribe(Listener)} or {@link #subscribe(Listener, SubscriptionOptions)}. + * No-op if the listener is not currently subscribed for this path. + * + * @param listener the listener to remove + */ + @NonBlocking + void unsubscribe(@NotNull Listener listener); + + /** + * Removes all listeners previously registered for this path. + */ + @NonBlocking + void unsubscribeAll(); + + /** + * Returns {@code true} if a value currently resolves at this path in the local + * object graph. This is a best-effort check evaluated at call time; the answer may + * change immediately afterwards as remote operations are applied. Useful as a + * guard before performing operations whose semantics depend on existence. + * + *

Complexity is O(n) in the path length because the path must be resolved. + * + * @return {@code true} if the path resolves to a value, {@code false} otherwise + */ + boolean exists(); + + /** + * Returns this {@code PathObject} wrapped as a {@link LiveMapPathObject}. + * + *

This is a best-effort cast - it does not validate that the underlying value + * at this path is a {@code LiveMap}. Read operations are always permitted on the + * returned wrapper; write or terminal operations that require resolution will fail + * at call time if the resolved value is not a {@code LiveMap}. + * + * @return a {@link LiveMapPathObject} view of this path + */ + @NotNull LiveMapPathObject asLiveMap(); + + /** + * Returns this {@code PathObject} wrapped as a {@link LiveCounterPathObject}. + * Best-effort cast; does not validate the underlying type at this path. + * + * @return a {@link LiveCounterPathObject} view of this path + */ + @NotNull LiveCounterPathObject asLiveCounter(); + + /** + * Returns this {@code PathObject} wrapped as a {@link NumberPathObject}. + * Best-effort cast; does not validate the underlying type at this path. + * + * @return a {@link NumberPathObject} view of this path + */ + @NotNull NumberPathObject asNumber(); + + /** + * Returns this {@code PathObject} wrapped as a {@link StringPathObject}. + * Best-effort cast; does not validate the underlying type at this path. + * + * @return a {@link StringPathObject} view of this path + */ + @NotNull StringPathObject asString(); + + /** + * Returns this {@code PathObject} wrapped as a {@link BooleanPathObject}. + * Best-effort cast; does not validate the underlying type at this path. + * + * @return a {@link BooleanPathObject} view of this path + */ + @NotNull BooleanPathObject asBoolean(); + + /** + * Returns this {@code PathObject} wrapped as a {@link BinaryPathObject}. + * Best-effort cast; does not validate the underlying type at this path. + * + * @return a {@link BinaryPathObject} view of this path + */ + @NotNull BinaryPathObject asBinary(); + + /** + * Returns this {@code PathObject} wrapped as a {@link JsonObjectPathObject}. + * Best-effort cast; does not validate the underlying type at this path. + * + * @return a {@link JsonObjectPathObject} view of this path + */ + @NotNull JsonObjectPathObject asJsonObject(); + + /** + * Returns this {@code PathObject} wrapped as a {@link JsonArrayPathObject}. + * Best-effort cast; does not validate the underlying type at this path. + * + * @return a {@link JsonArrayPathObject} view of this path + */ + @NotNull JsonArrayPathObject asJsonArray(); + + /** + * Listener interface for {@link PathObject#subscribe(Listener) path-based subscriptions}. + * + *

Spec: RTPO19a1 + */ + interface Listener { + /** + * Invoked when a change is applied at, or beneath, the subscribed path according + * to the configured {@link SubscriptionOptions}. + * + * @param event the event describing the change + */ + void onUpdated(@NotNull SubscriptionEvent event); + } + + /** + * Event delivered to {@link Listener#onUpdated(SubscriptionEvent)} when a change + * affects the subscribed path. + * + *

Spec: RTPO19e + */ + interface SubscriptionEvent { + /** + * Returns a {@link PathObject} pointing to the path where the change occurred. + * + *

Spec: RTPO19e1 + * + * @return the {@code PathObject} at the changed path + */ + @NotNull PathObject getObject(); + } + + /** + * Optional subscription options accepted by + * {@link PathObject#subscribe(Listener, SubscriptionOptions)}. + * + *

Spec: RTPO19c + */ + final class SubscriptionOptions { + + private final Integer depth; + + /** + * Creates options with the given {@code depth}. + * + * @param depth how many levels of path nesting below the subscribed path should + * trigger the listener; must be a positive integer if provided + */ + public SubscriptionOptions(@Nullable Integer depth) { + this.depth = depth; + } + + /** + * Returns the configured nesting depth, or {@code null} if not set. + * + *

Spec: RTPO19c1 + * + * @return the depth value, or {@code null} + */ + @Nullable + public Integer getDepth() { + return depth; + } + } +} diff --git a/lib/src/main/java/io/ably/lib/object/path/types/BinaryPathObject.java b/lib/src/main/java/io/ably/lib/object/path/types/BinaryPathObject.java new file mode 100644 index 000000000..0765f33e1 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/path/types/BinaryPathObject.java @@ -0,0 +1,27 @@ +package io.ably.lib.object.path.types; + +import io.ably.lib.object.path.PathObject; +import org.jetbrains.annotations.Nullable; + +/** + * A {@link PathObject} whose underlying value is expected to be a binary blob + * (a {@code byte[]}). + * + *

This is a terminal type. {@link PathObject#at(String)} remains purely + * navigational and will return a new {@link PathObject} whose later read/write + * operations fail to resolve. {@link PathObject#instance()} returns {@code null} + * because a primitive resolution does not produce a wrapped LiveObject. + * Only {@link #value()} and the inherited read APIs are useful here. + */ +public interface BinaryPathObject extends PathObject { + + /** + * Returns the binary value at this path, or {@code null} when the path does not + * resolve or resolves to a non-binary value. + * + *

Spec: RTPO7 + * + * @return the resolved bytes, or {@code null} + */ + byte @Nullable [] value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/path/types/BooleanPathObject.java b/lib/src/main/java/io/ably/lib/object/path/types/BooleanPathObject.java new file mode 100644 index 000000000..2d083e274 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/path/types/BooleanPathObject.java @@ -0,0 +1,27 @@ +package io.ably.lib.object.path.types; + +import io.ably.lib.object.path.PathObject; +import org.jetbrains.annotations.Nullable; + +/** + * A {@link PathObject} whose underlying value is expected to be a {@code Boolean}. + * + *

This is a terminal type. {@link PathObject#at(String)} remains purely + * navigational and will return a new {@link PathObject} whose later read/write + * operations fail to resolve. {@link PathObject#instance()} returns {@code null} + * because a primitive resolution does not produce a wrapped LiveObject. + * Only {@link #value()} and the inherited read APIs are useful here. + */ +public interface BooleanPathObject extends PathObject { + + /** + * Returns the boolean at this path, or {@code null} when the path does not resolve + * or resolves to a non-boolean value. + * + *

Spec: RTPO7 + * + * @return the resolved boolean, or {@code null} + */ + @Nullable + Boolean value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/path/types/JsonArrayPathObject.java b/lib/src/main/java/io/ably/lib/object/path/types/JsonArrayPathObject.java new file mode 100644 index 000000000..f6ffa77d0 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/path/types/JsonArrayPathObject.java @@ -0,0 +1,28 @@ +package io.ably.lib.object.path.types; + +import com.google.gson.JsonArray; +import io.ably.lib.object.path.PathObject; +import org.jetbrains.annotations.Nullable; + +/** + * A {@link PathObject} whose underlying value is expected to be a {@link JsonArray}. + * + *

This is a terminal type. {@link PathObject#at(String)} remains purely + * navigational and will return a new {@link PathObject} whose later read/write + * operations fail to resolve. {@link PathObject#instance()} returns {@code null} + * because a primitive resolution does not produce a wrapped LiveObject. + * Only {@link #value()} and the inherited read APIs are useful here. + */ +public interface JsonArrayPathObject extends PathObject { + + /** + * Returns the JSON array at this path, or {@code null} when the path does not + * resolve or resolves to a non-JsonArray value. + * + *

Spec: RTPO7 + * + * @return the resolved JsonArray, or {@code null} + */ + @Nullable + JsonArray value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/path/types/JsonObjectPathObject.java b/lib/src/main/java/io/ably/lib/object/path/types/JsonObjectPathObject.java new file mode 100644 index 000000000..3d9895240 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/path/types/JsonObjectPathObject.java @@ -0,0 +1,28 @@ +package io.ably.lib.object.path.types; + +import com.google.gson.JsonObject; +import io.ably.lib.object.path.PathObject; +import org.jetbrains.annotations.Nullable; + +/** + * A {@link PathObject} whose underlying value is expected to be a {@link JsonObject}. + * + *

This is a terminal type. {@link PathObject#at(String)} remains purely + * navigational and will return a new {@link PathObject} whose later read/write + * operations fail to resolve. {@link PathObject#instance()} returns {@code null} + * because a primitive resolution does not produce a wrapped LiveObject. + * Only {@link #value()} and the inherited read APIs are useful here. + */ +public interface JsonObjectPathObject extends PathObject { + + /** + * Returns the JSON object at this path, or {@code null} when the path does not + * resolve or resolves to a non-JsonObject value. + * + *

Spec: RTPO7 + * + * @return the resolved JsonObject, or {@code null} + */ + @Nullable + JsonObject value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/path/types/LiveCounterPathObject.java b/lib/src/main/java/io/ably/lib/object/path/types/LiveCounterPathObject.java new file mode 100644 index 000000000..a0893dd74 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/path/types/LiveCounterPathObject.java @@ -0,0 +1,86 @@ +package io.ably.lib.object.path.types; + +import io.ably.lib.object.path.PathObject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; + +/** + * A {@link PathObject} whose underlying value is expected to be a {@code LiveCounter}. + * Provides type-safe access to counter operations such as {@link #value()}, + * {@link #increment(Number)} and {@link #decrement(Number)}. + * + *

Counters are terminal nodes. {@link PathObject#at(String)} remains purely + * navigational and will return a new {@link PathObject} whose later read/write + * operations fail to resolve. + * + *

Operations are best-effort and resolve the path at call time. Read operations + * return {@code null} when the path does not resolve to a {@code LiveCounter}; write + * operations complete the returned {@link CompletableFuture} exceptionally with an + * {@code AblyException} (status 400, code 92007) in that case. + */ +public interface LiveCounterPathObject extends PathObject { + + /** + * Returns the current value of the {@code LiveCounter} at this path, or {@code null} + * when the path does not resolve to a {@code LiveCounter}. + * + *

Spec: RTPO7 / RTLC5 + * + * @return the counter value, or {@code null} + */ + @Nullable + Double value(); + + /** + * Increments the {@code LiveCounter} at this path by {@code 1}. Equivalent to + * calling {@link #increment(Number)} with {@code 1}. + * + *

Spec: RTPO17a1 (default {@code amount} of {@code 1}) + * + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture increment(); + + /** + * Increments the {@code LiveCounter} at this path by {@code amount}. + * + *

Sends a {@code COUNTER_INC} operation to the realtime system; the local state + * is updated when the operation is echoed back. The returned future completes + * exceptionally with an {@code AblyException} (status 400, code 92005) if the path + * cannot be resolved, or (status 400, code 92007) if the resolved value is not a + * {@code LiveCounter}. + * + *

Spec: RTPO17 + * + * @param amount the amount to add (may be negative) + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture increment(@NotNull Number amount); + + /** + * Decrements the {@code LiveCounter} at this path by {@code 1}. Equivalent to + * calling {@link #decrement(Number)} with {@code 1}. + * + *

Spec: RTPO18a1 (default {@code amount} of {@code 1}) + * + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture decrement(); + + /** + * Decrements the {@code LiveCounter} at this path by {@code amount}. Equivalent to + * calling {@link #increment(Number)} with a negated value. + * + *

Spec: RTPO18 + * + * @param amount the amount to subtract (may be negative) + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture decrement(@NotNull Number amount); +} diff --git a/lib/src/main/java/io/ably/lib/object/path/types/LiveMapPathObject.java b/lib/src/main/java/io/ably/lib/object/path/types/LiveMapPathObject.java new file mode 100644 index 000000000..5e04fda3e --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/path/types/LiveMapPathObject.java @@ -0,0 +1,125 @@ +package io.ably.lib.object.path.types; + +import io.ably.lib.object.path.PathObject; +import io.ably.lib.objects.type.map.LiveMapValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * A {@link PathObject} whose underlying value is expected to be a {@code LiveMap}. + * Provides type-safe access to map-specific operations such as {@link #get(String)}, + * {@link #entries()}, {@link #set(String, LiveMapValue)}, etc. + * + *

Calling {@code channel.objects.getRoot()}-equivalent navigation methods at the + * root of the graph always returns a {@code LiveMapPathObject}. + * + *

Operations on this type are best-effort: they resolve the path against the local + * LiveObjects graph at call time. Read operations return empty/null when the path does + * not resolve to a {@code LiveMap}; write operations complete the returned + * {@link CompletableFuture} exceptionally with an {@code AblyException} + * (status 400, code 92007) in that case. + */ +public interface LiveMapPathObject extends PathObject { + + /** + * Returns a new {@link PathObject} representing the child at {@code key} of the + * {@code LiveMap} at this path. Purely navigational - no resolution occurs. + * + *

Spec: RTPO5 + * + * @param key the child key to navigate to + * @return a {@link PathObject} pointing to {@code this.path + key} + */ + @NotNull + PathObject get(@NotNull String key); + + /** + * Returns the entries (key, child {@link PathObject}) of the {@code LiveMap} at + * this path. Each child path is produced as if by calling {@link #get(String)} with + * the corresponding key. + * + *

Returns an empty iterable when the path does not resolve to a {@code LiveMap}. + * + *

Spec: RTPO9 + * + * @return an unmodifiable iterable of map entries; empty when not a LiveMap + */ + @NotNull + @Unmodifiable + Iterable> entries(); + + /** + * Returns the keys of the {@code LiveMap} at this path. + * + *

Returns an empty iterable when the path does not resolve to a {@code LiveMap}. + * + *

Spec: RTPO10 + * + * @return an unmodifiable iterable of keys; empty when not a LiveMap + */ + @NotNull + @Unmodifiable + Iterable keys(); + + /** + * Returns the child {@link PathObject}s for each key in the {@code LiveMap} at this + * path. + * + *

Returns an empty iterable when the path does not resolve to a {@code LiveMap}. + * + *

Spec: RTPO11 + * + * @return an unmodifiable iterable of child paths; empty when not a LiveMap + */ + @NotNull + @Unmodifiable + Iterable values(); + + /** + * Returns the size of the {@code LiveMap} at this path, or {@code null} when the + * path does not resolve to a {@code LiveMap}. + * + *

Spec: RTPO12 + * + * @return the number of (non-tombstoned) entries, or {@code null} + */ + @Nullable + Long size(); + + /** + * Sets a key on the {@code LiveMap} at this path to the provided value. + * + *

Sends a {@code MAP_SET} operation to the realtime system; the local state is + * updated when the operation is echoed back. The returned future completes + * exceptionally with an {@code AblyException} (status 400, code 92005) if the path + * cannot be resolved, or (status 400, code 92007) if the resolved value is not a + * {@code LiveMap}. + * + *

Spec: RTPO15 + * + * @param key the key to set + * @param value the value to associate with {@code key} + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture set(@NotNull String key, @NotNull LiveMapValue value); + + /** + * Removes a key from the {@code LiveMap} at this path. + * + *

Sends a {@code MAP_REMOVE} operation to the realtime system; the local state + * is updated when the operation is echoed back. Same error conditions as + * {@link #set(String, LiveMapValue)} apply. + * + *

Spec: RTPO16 + * + * @param key the key to remove + * @return a future that completes when the operation has been acknowledged + */ + @NotNull + CompletableFuture remove(@NotNull String key); +} diff --git a/lib/src/main/java/io/ably/lib/object/path/types/NumberPathObject.java b/lib/src/main/java/io/ably/lib/object/path/types/NumberPathObject.java new file mode 100644 index 000000000..5f0b5986d --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/path/types/NumberPathObject.java @@ -0,0 +1,27 @@ +package io.ably.lib.object.path.types; + +import io.ably.lib.object.path.PathObject; +import org.jetbrains.annotations.Nullable; + +/** + * A {@link PathObject} whose underlying value is expected to be a {@code Number}. + * + *

This is a terminal type. {@link PathObject#at(String)} remains purely + * navigational and will return a new {@link PathObject} whose later read/write + * operations fail to resolve. {@link PathObject#instance()} returns {@code null} + * because a primitive resolution does not produce a wrapped LiveObject. + * Only {@link #value()} and the inherited read APIs are useful here. + */ +public interface NumberPathObject extends PathObject { + + /** + * Returns the number at this path, or {@code null} when the path does not resolve + * or resolves to a non-numeric value. + * + *

Spec: RTPO7 + * + * @return the resolved number, or {@code null} + */ + @Nullable + Number value(); +} diff --git a/lib/src/main/java/io/ably/lib/object/path/types/StringPathObject.java b/lib/src/main/java/io/ably/lib/object/path/types/StringPathObject.java new file mode 100644 index 000000000..c033219df --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/path/types/StringPathObject.java @@ -0,0 +1,27 @@ +package io.ably.lib.object.path.types; + +import io.ably.lib.object.path.PathObject; +import org.jetbrains.annotations.Nullable; + +/** + * A {@link PathObject} whose underlying value is expected to be a {@code String}. + * + *

This is a terminal type. {@link PathObject#at(String)} remains purely + * navigational and will return a new {@link PathObject} whose later read/write + * operations fail to resolve. {@link PathObject#instance()} returns {@code null} + * because a primitive resolution does not produce a wrapped LiveObject. + * Only {@link #value()} and the inherited read APIs are useful here. + */ +public interface StringPathObject extends PathObject { + + /** + * Returns the string at this path, or {@code null} when the path does not resolve + * or resolves to a non-string value. + * + *

Spec: RTPO7 + * + * @return the resolved string, or {@code null} + */ + @Nullable + String value(); +}