JsonData.java

package space.sunqian.fs.data.json;

import space.sunqian.annotation.Nonnull;
import space.sunqian.annotation.Nullable;
import space.sunqian.annotation.RetainedParam;
import space.sunqian.fs.Fs;
import space.sunqian.fs.data.ByteData;
import space.sunqian.fs.data.CharData;
import space.sunqian.fs.data.DataException;
import space.sunqian.fs.data.DataList;
import space.sunqian.fs.data.DataMap;
import space.sunqian.fs.object.convert.ObjectConverter;
import space.sunqian.fs.reflect.TypeRef;

import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

/**
 * The interface represents JSON data.
 *
 * @author sunqian
 */
public interface JsonData extends ByteData, CharData {

    /**
     * Returns a {@link JsonData} represents JSON null.
     *
     * @return a {@link JsonData} represents JSON null
     */
    static @Nonnull JsonData ofNull() {
        return JsonDataBack.JsonNull.INST;
    }

    /**
     * Returns a {@link JsonData} represents a JSON string whose value is the given string.
     *
     * @param string the given JSON string
     * @return a {@link JsonData} represents a JSON string whose value is the given string
     */
    static @Nonnull JsonData ofString(@Nonnull String string) {
        return new JsonDataBack.JsonString(string);
    }

    /**
     * Returns a {@link JsonData} represents a JSON number whose value is the given number.
     *
     * @param number the given JSON number
     * @return a {@link JsonData} represents a JSON number whose value is the given number
     */
    static @Nonnull JsonData ofNumber(@Nonnull Number number) {
        return new JsonDataBack.JsonNumber(number);
    }

    /**
     * Returns a {@link JsonData} represents a JSON boolean whose value is the given boolean.
     *
     * @param bool the given JSON boolean
     * @return a {@link JsonData} represents a JSON boolean whose value is the given boolean
     */
    static @Nonnull JsonData ofBoolean(boolean bool) {
        return bool ? JsonDataBack.JsonBoolean.TRUE : JsonDataBack.JsonBoolean.FALSE;
    }

    /**
     * Returns a {@link JsonData} represents a JSON object whose value is the given map.
     *
     * @param map the given map
     * @return a {@link JsonData} represents a JSON object whose value is the given map
     */
    static @Nonnull JsonData ofMap(@Nonnull @RetainedParam Map<@Nonnull String, @Nullable Object> map) {
        return new JsonDataBack.JsonObject(map);
    }

    /**
     * Returns a {@link JsonData} represents a JSON array whose value is the given list.
     *
     * @param array the given list
     * @return a {@link JsonData} represents a JSON array whose value is the given list
     */
    static @Nonnull JsonData ofList(@Nonnull @RetainedParam List<@Nullable Object> array) {
        return new JsonDataBack.JsonArray(array);
    }

    /**
     * Returns a {@link JsonData} represents a JSON array whose value is the given array.
     *
     * @param array the given array
     * @return a {@link JsonData} represents a JSON array whose value is the given array
     */
    static @Nonnull JsonData ofArray(@Nullable Object @Nonnull @RetainedParam ... array) {
        return new JsonDataBack.JsonArray(array);
    }

    /**
     * Returns the type of the current JSON data.
     *
     * @return the type of the current JSON data
     */
    @Nonnull
    JsonType type();

    /**
     * Returns {@code true} if the current JSON data is JSON null.
     *
     * @return {@code true} if the current JSON data is JSON null
     */
    default boolean isNull() {
        return type() == JsonType.NULL;
    }

    /**
     * Returns the string parsed from the current JSON data if the type of the current JSON data is JSON string.
     *
     * @return the string parsed from the current JSON data if the type of the current JSON data is JSON string
     * @throws JsonDataException if the current JSON data is not a JSON string
     */
    @Nonnull
    String asString() throws JsonDataException;

    /**
     * Returns a map parsed from the current JSON data if the type of the current JSON data is JSON object. Note the
     * returned map is mutable.
     *
     * @return a map parsed from the current JSON data if the type of the current JSON data is JSON object
     * @throws JsonDataException if the current JSON data is not a JSON object
     */
    @Nonnull
    Map<String, Object> asMap() throws JsonDataException;

    /**
     * Returns a list parsed from the current JSON data if the type of the current JSON data is JSON array. Note the
     * returned list is mutable.
     *
     * @return a list parsed from the current JSON data if the type of the current JSON data is JSON array
     * @throws JsonDataException if the current JSON data is not a JSON array
     */
    @Nonnull
    List<Object> asList() throws JsonDataException;

    /**
     * Returns an int value parsed from the current JSON data if the type of the current JSON data is JSON number.
     *
     * @return an int value parsed from the current JSON data if the type of the current JSON data is JSON number
     * @throws JsonDataException if the current JSON data is not a JSON number
     */
    default int asInt() throws JsonDataException {
        return asNumber().intValue();
    }

    /**
     * Returns a long value parsed from the current JSON data if the type of the current JSON data is JSON number.
     *
     * @return a long value parsed from the current JSON data if the type of the current JSON data is JSON number
     * @throws JsonDataException if the current JSON data is not a JSON number
     */
    default long asLong() throws JsonDataException {
        return asNumber().longValue();
    }

    /**
     * Returns a float value parsed from the current JSON data if the type of the current JSON data is JSON number.
     *
     * @return a float value parsed from the current JSON data if the type of the current JSON data is JSON number
     * @throws JsonDataException if the current JSON data is not a JSON number
     */
    default float asFloat() throws JsonDataException {
        return asNumber().floatValue();
    }

    /**
     * Returns a double value parsed from the current JSON data if the type of the current JSON data is JSON number.
     *
     * @return a double value parsed from the current JSON data if the type of the current JSON data is JSON number
     * @throws JsonDataException if the current JSON data is not a JSON number
     */
    default double asDouble() throws JsonDataException {
        return asNumber().doubleValue();
    }

    /**
     * Returns a {@link BigDecimal} value parsed from the current JSON data if the type of the current JSON data is JSON
     * number.
     *
     * @return a {@link BigDecimal} value parsed from the current JSON data if the type of the current JSON data is JSON
     * number
     * @throws JsonDataException if the current JSON data is not a JSON number
     */
    default @Nonnull BigDecimal asBigDecimal() throws JsonDataException {
        Number number = asNumber();
        return number instanceof BigDecimal ? (BigDecimal) number : new BigDecimal(number.toString());
    }

    /**
     * Returns a {@link Number} value parsed from the current JSON data if the type of the current JSON data is JSON
     * number.
     *
     * @return a {@link Number} value parsed from the current JSON data if the type of the current JSON data is JSON
     * number
     * @throws JsonDataException if the current JSON data is not a JSON number
     */
    @Nonnull
    Number asNumber() throws JsonDataException;

    /**
     * Returns a boolean value parsed from the current JSON data if the type of the current JSON data is JSON boolean.
     *
     * @return a boolean value parsed from the current JSON data if the type of the current JSON data is JSON boolean
     * @throws JsonDataException if the current JSON data is not a JSON boolean
     */
    boolean asBoolean() throws JsonDataException;

    /**
     * Returns a {@link DataMap} with the {@link ObjectConverter#defaultConverter()} to wrap the {@link #asMap()} if the
     * type of the current JSON data is JSON object.
     *
     * @return a {@link DataMap} with the {@link ObjectConverter#defaultConverter()} to wrap the {@link #asMap()} if the
     * type of the current JSON data is JSON object
     * @throws JsonDataException if the current JSON data is not a JSON object
     */
    default @Nonnull DataMap asDataMap() throws JsonDataException {
        return DataMap.wrap(asMap());
    }

    /**
     * Returns a {@link DataList} with the {@link ObjectConverter#defaultConverter()} to wrap the {@link #asList()} if
     * the type of the current JSON data is JSON array.
     *
     * @return a {@link DataList} with the {@link ObjectConverter#defaultConverter()} to wrap the {@link #asList()} if
     * the type of the current JSON data is JSON array
     * @throws JsonDataException if the current JSON data is not a JSON array
     */
    default @Nonnull DataList asDataList() throws JsonDataException {
        return DataList.wrap(asList());
    }

    /**
     * Converts this {@link JsonData} to a new object of the specified class.
     * <p>
     * This method supports {@code JSON Object}, {@code JSON Array}, {@code JSON String}, {@code JSON Number},
     * {@code JSON Boolean}, but not {@code JSON Null}.
     *
     * @param cls the specified class
     * @param <T> the type of the object to be returned`
     * @return a new object of the specified class
     * @throws DataException if an error occurs during the conversion
     * @implNote the default implementation uses {@link ObjectConverter#defaultConverter()} to convert types.
     */
    default <T> @Nonnull T toObject(Class<T> cls) throws DataException {
        return Fs.as(toObject((Type) cls));
    }

    /**
     * Converts this {@link JsonData} to a new object of the specified class.
     * <p>
     * This method supports {@code JSON Object}, {@code JSON Array}, {@code JSON String}, {@code JSON Number},
     * {@code JSON Boolean}, but not {@code JSON Null}.
     *
     * @param typeRef the reference of the specified class
     * @param <T>     the type of the object to be returned`
     * @return a new object of the specified class
     * @throws DataException if an error occurs during the conversion
     * @implNote the default implementation uses {@link ObjectConverter#defaultConverter()} to convert types.
     */
    default <T> @Nonnull T toObject(TypeRef<T> typeRef) throws DataException {
        return Fs.as(toObject(typeRef.type()));
    }

    /**
     * Converts this {@link JsonData} to a new object of the specified type.
     * <p>
     * This method supports {@code JSON Object}, {@code JSON Array}, {@code JSON String}, {@code JSON Number},
     * {@code JSON Boolean}, but not {@code JSON Null}.
     *
     * @param type the specified type
     * @return a new object of the specified type
     * @throws DataException if an error occurs during the conversion
     * @implNote the default implementation uses {@link ObjectConverter#defaultConverter()} to convert types.
     */
    @SuppressWarnings("EnhancedSwitchMigration")
    default @Nonnull Object toObject(Type type) throws DataException {
        JsonType jsonType = type();
        switch (jsonType) {
            case OBJECT:
                return asDataMap().toObject(type);
            case ARRAY:
                return asDataList().toObject(type);
            case STRING:
                return ObjectConverter.defaultConverter().convert(asString(), type);
            case NUMBER:
                return ObjectConverter.defaultConverter().convert(asNumber(), type);
            case BOOLEAN:
                return ObjectConverter.defaultConverter().convert(asBoolean(), type);
            default:
                throw new JsonDataException("Unsupported JSON type: " + jsonType + ".");
        }
    }

    /**
     * Returns the JSON string of the current JSON data.
     *
     * @return the JSON string of the current JSON data
     */
    @Nonnull
    String toString();
}