import { Opt, isNothing } from "@jet/environment/types/optional"; import * as serverData from "./server-data"; import * as media from "./data-structure"; import { JSONValue, MapLike, JSONData } from "./json-types"; import * as errors from "./errors"; // region Generic Attribute retrieval // region Attribute retrieval /** * Retrieve the specified attribute from the data, coercing it to a JSONData dictionary * * @param data The data from which to retrieve the attribute. * @param attributePath The path of the attribute. * @param defaultValue The object to return if the path search fails. * @returns The dictionary of data */ export function attributeAsDictionary( data: media.Data, attributePath?: serverData.ObjectPath, defaultValue?: MapLike, ): MapLike | null { if (serverData.isNull(data)) { return null; } return serverData.asDictionary(data.attributes, attributePath, defaultValue); } /** * Retrieve the specified attribute from the data, coercing it to an Interface * * @param data The data from which to retrieve the attribute. * @param attributePath The path of the attribute. * @param defaultValue The object to return if the path search fails. * @returns The dictionary of data as an interface */ export function attributeAsInterface( data: media.Data, attributePath?: serverData.ObjectPath, defaultValue?: JSONData, ): Interface | null { return attributeAsDictionary(data, attributePath, defaultValue) as unknown as Interface; } /** * Retrieve the specified attribute from the data as an array, coercing to an empty array if the object is not an array. * * @param data The data from which to retrieve the attribute. * @param attributePath The path of the attribute. * @returns {any[]} The attribute value as an array. */ export function attributeAsArrayOrEmpty( data: media.Data, attributePath?: serverData.ObjectPath, ): T[] { if (serverData.isNull(data)) { return []; } return serverData.asArrayOrEmpty(data.attributes, attributePath); } /** * Retrieve the specified attribute from the data as a string. * * @param data The data from which to retrieve the attribute. * @param attributePath The object path for the attribute. * @param policy The validation policy to use when resolving this value. * @returns {string} The attribute value as a string. */ export function attributeAsString( data: media.Data, attributePath?: serverData.ObjectPath, policy: serverData.ValidationPolicy = "coercible", ): Opt { if (serverData.isNull(data)) { return null; } return serverData.asString(data.attributes, attributePath, policy); } /** * Retrieve the specified meta from the data as a string. * * @param data The data from which to retrieve the attribute. * @param metaPath The object path for the meta. * @param policy The validation policy to use when resolving this value. * @returns {string} The meta value as a string. */ export function metaAsString( data: media.Data, metaPath?: serverData.ObjectPath, policy: serverData.ValidationPolicy = "coercible", ): Opt { if (serverData.isNull(data)) { return null; } return serverData.asString(data.meta, metaPath, policy); } /** * Retrieve the specified attribute from the data as a date. * * @param data The data from which to retrieve the attribute. * @param attributePath The object path for the attribute. * @param policy The validation policy to use when resolving this value. * @returns {Date} The attribute value as a date. */ export function attributeAsDate( data: media.Data, attributePath?: serverData.ObjectPath, policy: serverData.ValidationPolicy = "coercible", ): Opt { if (serverData.isNull(data)) { return null; } const dateString = serverData.asString(data.attributes, attributePath, policy); if (isNothing(dateString)) { return null; } return new Date(dateString); } /** * Retrieve the specified attribute from the data as a boolean. * * @param data The data from which to retrieve the attribute. * @param attributePath The path of the attribute. * @param policy The validation policy to use when resolving this value. * @returns {boolean} The attribute value as a boolean. */ export function attributeAsBoolean( data: media.Data, attributePath?: serverData.ObjectPath, policy: serverData.ValidationPolicy = "coercible", ): boolean | null { if (serverData.isNull(data)) { return null; } return serverData.asBoolean(data.attributes, attributePath, policy); } /** * Retrieve the specified attribute from the data as a boolean, which will be `false` if the attribute does not exist. * * @param data The data from which to retrieve the attribute. * @param attributePath The path of the attribute. * @returns {boolean} The attribute value as a boolean, coercing to `false` if the value is not present.. */ export function attributeAsBooleanOrFalse(data: media.Data, attributePath?: serverData.ObjectPath): boolean { if (serverData.isNull(data)) { return false; } return serverData.asBooleanOrFalse(data.attributes, attributePath); } /** * Retrieve the specified attribute from the data as a number. * * @param data The data from which to retrieve the attribute. * @param attributePath The path of the attribute. * @param policy The validation policy to use when resolving this value. * @returns {boolean} The attribute value as a number. */ export function attributeAsNumber( data: media.Data, attributePath?: serverData.ObjectPath, policy: serverData.ValidationPolicy = "coercible", ): Opt { if (serverData.isNull(data)) { return null; } return serverData.asNumber(data.attributes, attributePath, policy); } export function hasAttributes(data: media.Data): boolean { return !serverData.isNull(serverData.asDictionary(data, "attributes")); } /** * The canonical way to detect if an item from Media API is hydrated or not. * * @param data The data from which to retrieve the attributes. */ export function isNotHydrated(data: media.Data): boolean { return !hasAttributes(data); } // region Custom Attributes /** * Performs conversion for a custom variant of given attribute, if any are available. * @param attribute Attribute to get custom attribute key for, if any. */ export function attributeKeyAsCustomAttributeKey(attribute: string): string | undefined { return customAttributeMapping[attribute]; } /** * Whether or not given custom attributes key allows fallback to default page with AB testing treatment within a nondefault page. * This is to allow AB testing to affect only icons within custom product pages. */ export function attributeAllowsNonDefaultTreatmentInNonDefaultPage(customAttribute: string): boolean { return customAttribute === "customArtwork" || customAttribute === "customIconArtwork"; // Only the icon artwork. } /** * Defines mapping of attribute to custom attribute. */ const customAttributeMapping: { [key: string]: string } = { artwork: "customArtwork", iconArtwork: "customIconArtwork", screenshotsByType: "customScreenshotsByType", promotionalText: "customPromotionalText", videoPreviewsByType: "customVideoPreviewsByType", customScreenshotsByTypeForAd: "customScreenshotsByTypeForAd", customVideoPreviewsByTypeForAd: "customVideoPreviewsByTypeForAd", }; export function requiredAttributeAsString(data: media.Data, attributePath: serverData.ObjectPath): string { const value = attributeAsString(data, attributePath); if (isNothing(value)) { throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath)); } return value; } export function requiredAttributeAsDate(data: media.Data, attributePath: serverData.ObjectPath): Date { const value = attributeAsDate(data, attributePath); if (isNothing(value)) { throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath)); } return value; } export function requiredAttributeAsDictionary( data: media.Data, attributePath: serverData.ObjectPath, ): MapLike { const value: MapLike | null = attributeAsDictionary(data, attributePath); if (isNothing(value)) { throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath)); } return value; } export function requiredMeta(data: media.Data): MapLike { const value = serverData.asDictionary(data, "meta"); if (isNothing(value)) { throw new errors.MissingFieldError(data, "meta"); } return value; } export function requiredMetaAttributeAsString(data: media.Data, attributePath: serverData.ObjectPath): string { const meta = requiredMeta(data); const value = serverData.asString(meta, attributePath); if (isNothing(value)) { throw new errors.MissingFieldError(data, concatObjectPaths("meta", attributePath)); } return value; } export function requiredMetaAttributeAsNumber(data: media.Data, attributePath: serverData.ObjectPath): number { const meta = requiredMeta(data); const value = serverData.asNumber(meta, attributePath); if (isNothing(value)) { throw new errors.MissingFieldError(data, concatObjectPaths("meta", attributePath)); } return value; } export function concatObjectPaths(prefix: serverData.ObjectPath, suffix: serverData.ObjectPath): serverData.ObjectPath { let finalPath: string[]; if (Array.isArray(prefix)) { finalPath = prefix; } else { finalPath = [prefix]; } if (Array.isArray(suffix)) { finalPath.push(...suffix); } else { finalPath.push(suffix); } return finalPath; } // endregion