//
//  server-data.ts
//  AppStoreKit
//
//  Created by Kevin MacWhinnie on 8/17/16.
//  Copyright (c) 2016 Apple Inc. All rights reserved.
//
import * as validation from "@jet/environment/json/validation";
import { isNothing } from "@jet/environment/types/optional";
/**
 * Returns the string representation of a given object path.
 * @param path  The object path to coerce to a string.
 * @returns A string representation of `path`.
 */
export function objectPathToString(path) {
    if (isNull(path)) {
        return null;
    }
    else if (Array.isArray(path)) {
        return path.join(".");
    }
    else {
        return path;
    }
}
const PARSED_PATH_CACHE = {};
/**
 *  Traverse a nested JSON object structure, short-circuiting
 *  when finding `undefined` or `null` values. Usage:
 *
 *      const object = {x: {y: {z: 42}}};
 *      const meaningOfLife = serverData.traverse(object, 'x.y.z');
 *
 *  @param  object  The JSON object to traverse.
 *  @param  path    The path to search. If falsy, `object` will be returned without being traversed.
 *  @param  defaultValue The object to return if the path search fails.
 *  @return The value at `path` if found; default value otherwise.
 */
export function traverse(object, path, defaultValue) {
    if (object === undefined || object === null) {
        return defaultValue;
    }
    if (!path) {
        return object;
    }
    let components;
    if (typeof path === "string") {
        components = PARSED_PATH_CACHE[path];
        if (!components) {
            // Fast Path: If the path contains only a single component, we can skip
            //            all of the work below here and speed up storefronts that
            //            don't have JIT compilation enabled.
            if (!path.includes(".")) {
                const value = object[path];
                if (value !== undefined && value !== null) {
                    return value;
                }
                else {
                    return defaultValue;
                }
            }
            components = path.split(".");
            PARSED_PATH_CACHE[path] = components;
        }
    }
    else {
        components = path;
    }
    let current = object;
    for (const component of components) {
        current = current[component];
        if (current === undefined || current === null) {
            return defaultValue;
        }
    }
    return current;
}
// endregion
// region Nullability
/**
 *  Returns a bool indicating whether or not a given object null or undefined.
 *  @param  object  The object to test.
 *  @return true if the object is null or undefined; false otherwise.
 */
export function isNull(object) {
    return object === null || object === undefined;
}
/**
 * Returns a bool indicating whether or not a given object is null or empty.
 * @param object The object to test
 * @return true if object is null or empty; false otherwise.
 */
export function isNullOrEmpty(object) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return isNull(object) || Object.keys(object).length === 0;
}
/**
 *  Returns a bool indicating whether or not a given object is non-null.
 *  @param  object  The object to test.
 *  @return true if the object is not null or undefined; false otherwise.
 */
export function isDefinedNonNull(object) {
    return typeof object !== "undefined" && object !== null;
}
/**
 *  Returns a bool indicating whether or not a given object is non-null or empty.
 *  @param  object  The object to test.
 *  @return true if the object is not null or undefined and not empty; false otherwise.
 */
export function isDefinedNonNullNonEmpty(object) {
    if (isNothing(object)) {
        return false;
    }
    if (typeof object === "string") {
        return object.length > 0;
    }
    else if (Array.isArray(object)) {
        return object.length > 0;
    }
    else {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        return Object.keys(object).length !== 0;
    }
}
/**
 *  Returns a bool indicating whether or not a given object is non-null or empty.
 *  @param  object  The object to test.
 *  @return true if the object is not null or undefined and not empty; false otherwise.
 */
export function isSetDefinedNonNullNonEmpty(object) {
    return isDefinedNonNull(object) && object.size > 0;
}
/**
 * Checks if the passed string or number is a number
 *
 * @param value The value to check
 * @return True if the value is an number, false if not
 */
export function isNumber(value) {
    if (isNull(value)) {
        return false;
    }
    let valueToCheck;
    if (typeof value === "string") {
        valueToCheck = parseInt(value);
    }
    else {
        valueToCheck = value;
    }
    return !Number.isNaN(valueToCheck);
}
/**
 * Checks if the value is a string
 *
 * @param value The value to check
 * @return True if the value is a string, false if not
 */
export function isString(value) {
    if (isNothing(value)) {
        return false;
    }
    return typeof value === "string";
}
/**
 *  Returns a bool indicating whether or not a given object is defined but empty.
 *  @param  object  The object to test.
 *  @return true if the object is not null and empty; false otherwise.
 */
export function isArrayDefinedNonNullAndEmpty(object) {
    return isDefinedNonNull(object) && object.length === 0;
}
// endregion
// region Defaulting Casts
/**
 * Check that a given object is an array, substituting an empty array if not.
 * @param object    The object to coerce.
 * @param path      The path to traverse on `object` to find an array.
 *                  Omit this parameter if `object` is itself an array.
 * @returns An untyped array.
 */
export function asArrayOrEmpty(object, path) {
    var _a;
    return (_a = asArray(object, path, [])) !== null && _a !== void 0 ? _a : [];
}
/**
 * Check that a given object is a boolean, substituting the value `false` if not.
 * @param object    The object to coerce.
 * @param path      The path to traverse on `object` to find a boolean.
 *                  Omit this parameter if `object` is itself a boolean.
 * @returns A boolean from `object`, or defaults to `false`.
 */
export function asBooleanOrFalse(object, path) {
    return asBooleanWithDefault(object, false, path);
}
/**
 * Check that a given object is a boolean, substituting the value `false` if not.
 * @param object    The object to coerce.
 * @param path      The path to traverse on `object` to find a boolean.
 *                  Omit this parameter if `object` is itself a boolean.
 * @returns A boolean from `object`, or the provided default.
 */
export function asBooleanWithDefault(object, defaultValue, path) {
    const target = traverse(object, path, null);
    if (typeof target === "boolean") {
        return target;
    }
    else {
        if (!isNull(target)) {
            validation.context("asBooleanWithDefault", () => {
                validation.unexpectedType("defaultValue", "boolean", target, objectPathToString(path));
            });
        }
        return defaultValue;
    }
}
/**
 * Safely coerce an object into a string.
 * @param object            The object to coerce.
 * @param path              The path to traverse on `object` to find a string.
 *                          Omit this parameter if `object` is itself a string.
 * @param policy            The validation policy to use when resolving this value
 * @returns A string from `object`, or `null` if `object` is null.
 */
export function asString(object, path, policy = "coercible") {
    const target = traverse(object, path, null);
    if (isNull(target)) {
        return target;
    }
    else if (typeof target === "string") {
        return target;
    }
    else {
        // We don't consider arbitrary objects as convertable to strings even through they will result in some value
        const coercedValue = typeof target === "object" ? null : String(target);
        switch (policy) {
            case "strict": {
                validation.context("asString", () => {
                    validation.unexpectedType("coercedValue", "string", target, objectPathToString(path));
                });
                break;
            }
            case "coercible": {
                if (isNull(coercedValue)) {
                    validation.context("asString", () => {
                        validation.unexpectedType("coercedValue", "string", target, objectPathToString(path));
                    });
                }
                break;
            }
            case "none":
            default: {
                break;
            }
        }
        return coercedValue;
    }
}
/**
 * Safely coerce an object into a number.
 * @param object            The object to coerce.
 * @param path              The path to traverse on `object` to find a number.
 *                          Omit this parameter if `object` is itself a number.
 * @param policy            The validation policy to use when resolving this value
 * @returns A number from `object`, or `null` if `object` is null.
 */
export function asNumber(object, path, policy = "coercible") {
    const target = traverse(object, path, null);
    if (isNull(target) || typeof target === "number") {
        return target;
    }
    else {
        const coercedValue = Number(target);
        switch (policy) {
            case "strict": {
                validation.context("asNumber", () => {
                    validation.unexpectedType("coercedValue", "number", target, objectPathToString(path));
                });
                break;
            }
            case "coercible": {
                if (isNaN(coercedValue)) {
                    validation.context("asNumber", () => {
                        validation.unexpectedType("coercedValue", "number", target, objectPathToString(path));
                    });
                    return null;
                }
                break;
            }
            case "none":
            default: {
                break;
            }
        }
        return coercedValue;
    }
}
/**
 * Safely coerce an object into a dictionary.
 * @param object            The object to coerce.
 * @param path              The path to traverse on `object` to find the dictionary.
 *                          Omit this parameter if `object` is itself a dictionary.
 * @param  defaultValue The object to return if the path search fails.
 * @returns A sub-dictionary from `object`, or `null` if `object` is null.
 */
export function asDictionary(object, path, defaultValue) {
    const target = traverse(object, path, null);
    if (target instanceof Object && !Array.isArray(target)) {
        // Note: It's too expensive to actually validate this is a dictionary of { string : Type } at run time
        return target;
    }
    else {
        if (!isNull(target)) {
            validation.context("asDictionary", () => {
                validation.unexpectedType("defaultValue", "object", target, objectPathToString(path));
            });
        }
        if (isDefinedNonNull(defaultValue)) {
            return defaultValue;
        }
        return null;
    }
}
/**
 * Coerce an object into an array
 * @param object    The object to coerce.
 * @param path      The path to traverse on `object` to find an array.
 *                  Omit this parameter if `object` is itself an array.
 * @returns An untyped array.
 */
export function asArray(object, path, defaultValue) {
    const target = traverse(object, path, null);
    if (Array.isArray(target)) {
        // Note: This is kind of a nasty cast, but I don't think we want to validate that everything is of type T
        return target;
    }
    else {
        if (!isNull(target)) {
            validation.context("asArray", () => {
                validation.unexpectedType("defaultValue", "array", target, objectPathToString(path));
            });
        }
        if (isDefinedNonNull(defaultValue)) {
            return defaultValue;
        }
        return null;
    }
}
/**
 * Safely coerce an object into a given interface.
 * @param object            The object to coerce.
 * @param path              The path to traverse on `object` to find a string.
 *                          Omit this parameter if `object` is itself a string.
 * @param  defaultValue The object to return if the path search fails.
 * @returns A sub-dictionary from `object`, or `null` if `object` is null.
 */
export function asInterface(object, path, defaultValue) {
    return asDictionary(object, path, defaultValue);
}
/**
 * Coerce an object into a boolean.
 * @param object            The object to coerce.
 * @param path              The path to traverse on `object` to find a boolean.
 *                          Omit this parameter if `object` is itself a boolean.
 * @param policy            The validation policy to use when resolving this value
 * @returns A boolean from `object`, or `null` if `object` is null.
 * @note This is distinct from `asBooleanOrFalse` in that it doesn't default to false,
 * and it tries to convert string boolean values into actual boolean types
 */
export function asBoolean(object, path, policy = "coercible") {
    const target = traverse(object, path, null);
    // Value was null
    if (isNull(target)) {
        return null;
    }
    // Value was boolean.
    if (typeof target === "boolean") {
        return target;
    }
    // Value was string.
    if (typeof target === "string") {
        if (target === "true") {
            return true;
        }
        else if (target === "false") {
            return false;
        }
    }
    // Else coerce.
    const coercedValue = Boolean(target);
    switch (policy) {
        case "strict": {
            validation.context("asBoolean", () => {
                validation.unexpectedType("coercedValue", "number", target, objectPathToString(path));
            });
            break;
        }
        case "coercible": {
            if (isNull(coercedValue)) {
                validation.context("asBoolean", () => {
                    validation.unexpectedType("coercedValue", "number", target, objectPathToString(path));
                });
                return null;
            }
            break;
        }
        case "none":
        default: {
            break;
        }
    }
    return coercedValue;
}
/**
 * Attempts to coerce the passed value to a JSONValue
 *
 * Note: due to performance concerns this does not perform a deep inspection of Objects or Arrays.
 *
 * @param value The value to coerce
 * @return A JSONValue or null if value is not a valid JSONValue type
 */
export function asJSONValue(value) {
    if (value === null || value === undefined) {
        return null;
    }
    switch (typeof value) {
        case "string":
        case "number":
        case "boolean":
            return value;
        case "object":
            // Note: It's too expensive to actually validate this is an array of JSONValues at run time
            if (Array.isArray(value)) {
                return value;
            }
            // Note: It's too expensive to actually validate this is a dictionary of { string : JSONValue } at run time
            return value;
        default:
            validation.context("asJSONValue", () => {
                validation.unexpectedType("defaultValue", "JSONValue", typeof value);
            });
            return null;
    }
}
/**
 * Attempts to coerce the passed value to JSONData
 *
 * @param value The value to coerce
 * @return A JSONData or null if the value is not a valid JSONData object
 */
export function asJSONData(value) {
    if (value === null || value === undefined) {
        return null;
    }
    if (value instanceof Object && !Array.isArray(value)) {
        // Note: It's too expensive to actually validate this is a dictionary of { string : Type } at run time
        return value;
    }
    validation.context("asJSONValue", () => {
        validation.unexpectedType("defaultValue", "object", typeof value);
    });
    return null;
}
// endregion
//# sourceMappingURL=server-data.js.map