Files
main/node_modules/@apple-media-services/media-api/src/models/attributes.ts
2025-11-04 05:03:50 +08:00

290 lines
9.7 KiB
TypeScript

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<Type extends JSONValue>(
data: media.Data,
attributePath?: serverData.ObjectPath,
defaultValue?: MapLike<Type>,
): MapLike<Type> | 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<Interface>(
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<T extends JSONValue>(
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<string> {
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<string> {
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<Date> {
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<number> {
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<Type extends JSONValue>(
data: media.Data,
attributePath: serverData.ObjectPath,
): MapLike<Type> {
const value: MapLike<Type> | null = attributeAsDictionary(data, attributePath);
if (isNothing(value)) {
throw new errors.MissingFieldError(data, concatObjectPaths("attributes", attributePath));
}
return value;
}
export function requiredMeta(data: media.Data): MapLike<JSONValue> {
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