summaryrefslogtreecommitdiff
path: root/node_modules/@apple-media-services/media-api/src/models
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@apple-media-services/media-api/src/models')
-rw-r--r--node_modules/@apple-media-services/media-api/src/models/attributes.ts289
-rw-r--r--node_modules/@apple-media-services/media-api/src/models/server-data.ts476
-rw-r--r--node_modules/@apple-media-services/media-api/src/models/urls.ts469
3 files changed, 1234 insertions, 0 deletions
diff --git a/node_modules/@apple-media-services/media-api/src/models/attributes.ts b/node_modules/@apple-media-services/media-api/src/models/attributes.ts
new file mode 100644
index 0000000..edb5fca
--- /dev/null
+++ b/node_modules/@apple-media-services/media-api/src/models/attributes.ts
@@ -0,0 +1,289 @@
+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
diff --git a/node_modules/@apple-media-services/media-api/src/models/server-data.ts b/node_modules/@apple-media-services/media-api/src/models/server-data.ts
new file mode 100644
index 0000000..a1aca3f
--- /dev/null
+++ b/node_modules/@apple-media-services/media-api/src/models/server-data.ts
@@ -0,0 +1,476 @@
+//
+// server-data.ts
+// AppStoreKit
+//
+// Created by Kevin MacWhinnie on 8/17/16.
+// Copyright (c) 2016 Apple Inc. All rights reserved.
+//
+
+// TODO: Replace this utility for JSON Parsing
+import * as validation from "@jet/environment/json/validation";
+import { Nothing, Opt, isNothing } from "@jet/environment/types/optional";
+import { JSONArray, JSONData, JSONValue, MapLike } from "./json-types";
+
+// region Traversal
+
+/**
+ * Union type that describes the possible representations for an object traversal path.
+ */
+export type ObjectPath = string | string[];
+
+/**
+ * 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: Opt<ObjectPath>): Opt<string> {
+ if (isNull(path)) {
+ return null;
+ } else if (Array.isArray(path)) {
+ return path.join(".");
+ } else {
+ return path;
+ }
+}
+
+const PARSED_PATH_CACHE: { [key: string]: string[] } = {};
+
+/**
+ * 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: JSONValue, path?: ObjectPath, defaultValue?: JSONValue): JSONValue {
+ if (object === undefined || object === null) {
+ return defaultValue;
+ }
+
+ if (isNullOrEmpty(path)) {
+ return object;
+ }
+
+ let components: string[];
+ if (typeof path === "string") {
+ components = PARSED_PATH_CACHE[path];
+ if (isNullOrEmpty(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: JSONValue = 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<Type>(object: Type | Nothing): object is Nothing {
+ 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<Type>(object: Type | Nothing): object is Nothing {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return isNull(object) || Object.keys(object as any).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<Type>(object: Type | null | undefined): object is Type {
+ 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<Type>(object: Type | Nothing): object is Type {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return isDefinedNonNull(object) && Object.keys(object as any).length !== 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: number | string | null | undefined): value is number {
+ if (isNull(value)) {
+ return false;
+ }
+
+ let valueToCheck;
+ if (typeof value === "string") {
+ valueToCheck = parseInt(value);
+ } else {
+ valueToCheck = value;
+ }
+
+ return !Number.isNaN(valueToCheck);
+}
+
+/**
+ * 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<Type extends JSONArray>(object: Type | null | undefined): object is Type {
+ 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<T extends JSONValue>(object: JSONValue, path?: ObjectPath): T[] {
+ 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 as T[];
+ } else {
+ if (!isNull(target)) {
+ validation.context("asArrayOrEmpty", () => {
+ validation.unexpectedType("defaultValue", "array", target, objectPathToString(path));
+ });
+ }
+ return [];
+ }
+}
+
+/**
+ * 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: JSONValue, path?: ObjectPath): boolean {
+ const target = traverse(object, path, null);
+ if (typeof target === "boolean") {
+ return target;
+ } else {
+ if (!isNull(target)) {
+ validation.context("asBooleanOrFalse", () => {
+ validation.unexpectedType("defaultValue", "boolean", target, objectPathToString(path));
+ });
+ }
+ return false;
+ }
+}
+
+// endregion
+
+// region Coercing Casts
+
+export type ValidationPolicy = "strict" | "coercible" | "none";
+
+/**
+ * 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: JSONValue, path?: ObjectPath, policy: ValidationPolicy = "coercible"): Opt<string> {
+ 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 date.
+ * @param object The object to coerce.
+ * @param path The path to traverse on `object` to find a date.
+ * @param policy The validation policy to use when resolving this value
+ * @returns A date from `object`, or `null` if `object` is null.
+ */
+export function asDate(object: JSONValue, path?: ObjectPath, policy: ValidationPolicy = "coercible"): Opt<Date> {
+ const dateString = asString(object, path, policy);
+ if (isNothing(dateString)) {
+ return null;
+ }
+ return new Date(dateString);
+}
+
+/**
+ * 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: JSONValue, path?: ObjectPath, policy: ValidationPolicy = "coercible"): Opt<number> {
+ 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<Type extends JSONValue>(
+ object: JSONValue,
+ path?: ObjectPath,
+ defaultValue?: MapLike<Type>,
+): MapLike<Type> | null {
+ 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 as MapLike<Type>;
+ } else {
+ if (!isNull(target)) {
+ validation.context("asDictionary", () => {
+ validation.unexpectedType("defaultValue", "object", 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<Interface>(
+ object: JSONValue,
+ path?: ObjectPath,
+ defaultValue?: JSONData,
+): Interface | null {
+ return asDictionary(object, path, defaultValue) as unknown as Interface;
+}
+
+/**
+ * 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: JSONValue,
+ path?: ObjectPath,
+ policy: ValidationPolicy = "coercible",
+): boolean | null {
+ 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: unknown): JSONValue | null {
+ if (value === null || value === undefined) {
+ return null;
+ }
+ switch (typeof value) {
+ case "string":
+ case "number":
+ case "boolean":
+ return value as JSONValue;
+ 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 as JSONValue;
+ }
+ // Note: It's too expensive to actually validate this is a dictionary of { string : JSONValue } at run time
+ return value as JSONValue;
+ 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: unknown): JSONData | null {
+ 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 as JSONData;
+ }
+ validation.context("asJSONValue", () => {
+ validation.unexpectedType("defaultValue", "object", typeof value);
+ });
+ return null;
+}
+
+// endregion
diff --git a/node_modules/@apple-media-services/media-api/src/models/urls.ts b/node_modules/@apple-media-services/media-api/src/models/urls.ts
new file mode 100644
index 0000000..c1c09f1
--- /dev/null
+++ b/node_modules/@apple-media-services/media-api/src/models/urls.ts
@@ -0,0 +1,469 @@
+/**
+ * Created by keithpk on 12/2/16.
+ */
+
+import { isNothing, Nothing, Opt } from "@jet/environment/types/optional";
+import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "./server-data";
+
+export type Query = {
+ [key: string]: string | undefined;
+};
+
+export type URLComponent = "protocol" | "username" | "password" | "host" | "port" | "pathname" | "query" | "hash";
+
+const protocolRegex = /^([a-z][a-z0-9.+-]*:)(\/\/)?([\S\s]*)/i;
+const queryParamRegex = /([^=?&]+)=?([^&]*)/g;
+const componentOrder: URLComponent[] = ["hash", "query", "pathname", "host"];
+
+type URLSplitStyle = "prefix" | "suffix";
+
+type URLSplitResult = {
+ result?: string;
+ remainder: string;
+};
+
+function splitUrlComponent(input: string, marker: string, style: URLSplitStyle): URLSplitResult {
+ const index = input.indexOf(marker);
+ let result;
+ let remainder = input;
+ if (index !== -1) {
+ const prefix = input.slice(0, index);
+ const suffix = input.slice(index + marker.length, input.length);
+
+ if (style === "prefix") {
+ result = prefix;
+ remainder = suffix;
+ } else {
+ result = suffix;
+ remainder = prefix;
+ }
+ }
+
+ // log("Token: " + marker + " String: " + input, " Result: " + result + " Remainder: " + remainder)
+
+ return {
+ result: result,
+ remainder: remainder,
+ };
+}
+
+export class URL {
+ protocol?: Opt<string>;
+ username: string;
+ password: string;
+ host?: Opt<string>;
+ port: string;
+ pathname?: Opt<string>;
+ query?: Query = {};
+ hash?: string;
+
+ constructor(url?: string) {
+ if (isNullOrEmpty(url)) {
+ return;
+ }
+
+ // Split the protocol from the rest of the urls
+ let remainder = url;
+ const match = protocolRegex.exec(url);
+ if (match != null) {
+ // Pull out the protocol
+ let protocol = match[1];
+ if (protocol) {
+ protocol = protocol.split(":")[0];
+ }
+
+ this.protocol = protocol;
+
+ // Save the remainder
+ remainder = match[3];
+ }
+
+ // Then match each component in a specific order
+ let parse: URLSplitResult = { remainder: remainder, result: undefined };
+ for (const component of componentOrder) {
+ if (!parse.remainder) {
+ break;
+ }
+
+ switch (component) {
+ case "hash": {
+ parse = splitUrlComponent(parse.remainder, "#", "suffix");
+ this.hash = parse.result;
+ break;
+ }
+ case "query": {
+ parse = splitUrlComponent(parse.remainder, "?", "suffix");
+ if (isDefinedNonNullNonEmpty(parse.result)) {
+ this.query = URL.queryFromString(parse.result);
+ }
+ break;
+ }
+ case "pathname": {
+ parse = splitUrlComponent(parse.remainder, "/", "suffix");
+
+ if (isDefinedNonNullNonEmpty(parse.result)) {
+ // Replace the initial /, since paths require it
+ this.pathname = "/" + parse.result;
+ }
+ break;
+ }
+ case "host": {
+ if (parse.remainder) {
+ const authorityParse = splitUrlComponent(parse.remainder, "@", "prefix");
+ const userInfo = authorityParse.result;
+ const hostPort = authorityParse.remainder;
+ if (isDefinedNonNullNonEmpty(userInfo)) {
+ const userInfoSplit = userInfo.split(":");
+ this.username = decodeURIComponent(userInfoSplit[0]);
+ this.password = decodeURIComponent(userInfoSplit[1]);
+ }
+
+ if (hostPort) {
+ const hostPortSplit = hostPort.split(":");
+ this.host = hostPortSplit[0];
+ this.port = hostPortSplit[1];
+ }
+ }
+ break;
+ }
+ default: {
+ throw new Error("Unhandled case!");
+ }
+ }
+ }
+ }
+
+ set(component: URLComponent, value: string | Query): URL {
+ if (isNullOrEmpty(value)) {
+ return this;
+ }
+
+ if (component === "query") {
+ if (typeof value === "string") {
+ value = URL.queryFromString(value);
+ }
+ }
+
+ switch (component) {
+ // Exhaustive match to make sure TS property minifiers and other
+ // transformer plugins do not break this code.
+ case "protocol":
+ this.protocol = value as string;
+ break;
+ case "username":
+ this.username = value as string;
+ break;
+ case "password":
+ this.password = value as string;
+ break;
+ case "port":
+ this.port = value as string;
+ break;
+ case "pathname":
+ this.pathname = value as string;
+ break;
+ case "query":
+ this.query = value as Query;
+ break;
+ case "hash":
+ this.hash = value as string;
+ break;
+ default:
+ // The fallback for component which is not a property of URL object.
+ this[component] = value as string;
+ break;
+ }
+ return this;
+ }
+
+ private get(component: URLComponent): string | Query | Nothing {
+ switch (component) {
+ // Exhaustive match to make sure TS property minifiers and other
+ // transformer plugins do not break this code.
+ case "protocol":
+ return this.protocol;
+ case "username":
+ return this.username;
+ case "password":
+ return this.password;
+ case "port":
+ return this.port;
+ case "pathname":
+ return this.pathname;
+ case "query":
+ return this.query;
+ case "hash":
+ return this.hash;
+ default:
+ // The fallback for component which is not a property of URL object.
+ return this[component];
+ }
+ }
+
+ append(component: URLComponent, value: string | Query): URL {
+ const existingValue = this.get(component);
+ let newValue;
+
+ if (component === "query") {
+ if (typeof value === "string") {
+ value = URL.queryFromString(value);
+ }
+
+ if (typeof existingValue === "string") {
+ newValue = { existingValue, ...value };
+ } else {
+ newValue = { ...existingValue, ...value };
+ }
+ } else {
+ let existingValueString = existingValue as string;
+
+ if (!existingValueString) {
+ existingValueString = "";
+ }
+
+ let newValueString = existingValueString;
+
+ if (component === "pathname") {
+ const pathLength = existingValueString.length;
+ if (!pathLength || existingValueString[pathLength - 1] !== "/") {
+ newValueString += "/";
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-base-to-string
+ newValueString += value;
+ newValue = newValueString;
+ }
+
+ return this.set(component, newValue);
+ }
+
+ param(key: string, value?: string): URL {
+ if (!key) {
+ return this;
+ }
+ if (this.query == null) {
+ this.query = {};
+ }
+ this.query[key] = value;
+ return this;
+ }
+
+ removeParam(key: string): URL {
+ if (!key || this.query == null) {
+ return this;
+ }
+ if (this.query[key] !== undefined) {
+ delete this.query[key];
+ }
+ return this;
+ }
+
+ /**
+ * Push a new string value onto the path for this url
+ * @returns URL this object with the updated path.
+ */
+ path(value: string): URL {
+ return this.append("pathname", value);
+ }
+
+ pathExtension(): Opt<string> {
+ // Extract path extension if one exists
+ if (isNothing(this.pathname)) {
+ return null;
+ }
+
+ const lastFilenameComponents = this.pathname
+ .split("/")
+ .filter((item) => item.length > 0) // Remove any double or trailing slashes
+ .pop()
+ ?.split(".");
+ if (lastFilenameComponents === undefined) {
+ return null;
+ }
+ if (
+ lastFilenameComponents.filter((part) => {
+ return part !== "";
+ }).length < 2 // Remove any empty parts (e.g. .ssh_config -> ["ssh_config"])
+ ) {
+ return null;
+ }
+
+ return lastFilenameComponents.pop();
+ }
+
+ /**
+ * Returns the path components of the URL
+ * @returns An array of non-empty path components from `urls`.
+ */
+ pathComponents(): string[] {
+ if (isNullOrEmpty(this.pathname)) {
+ return [];
+ }
+
+ return this.pathname.split("/").filter((component) => component.length > 0);
+ }
+
+ /**
+ * Returns the last path component from this url, updating the url to not include this path component
+ * @returns String the last path component from this url.
+ */
+ popPathComponent(): string | null {
+ if (isNullOrEmpty(this.pathname)) {
+ return null;
+ }
+
+ const lastPathComponent = this.pathname.slice(this.pathname.lastIndexOf("/") + 1);
+
+ if (lastPathComponent.length === 0) {
+ return null;
+ }
+
+ this.pathname = this.pathname.slice(0, this.pathname.lastIndexOf("/"));
+
+ return lastPathComponent;
+ }
+
+ /**
+ * Same as toString
+ *
+ * @returns {string} A string representation of the URL
+ */
+ build(): string {
+ return this.toString();
+ }
+
+ /**
+ * Converts the URL to a string
+ *
+ * @returns {string} A string representation of the URL
+ */
+ toString(): string {
+ let url = "";
+
+ if (isDefinedNonNullNonEmpty(this.protocol)) {
+ url += this.protocol + "://";
+ }
+
+ if (this.username) {
+ url += encodeURIComponent(this.username);
+
+ if (this.password) {
+ url += ":" + encodeURIComponent(this.password);
+ }
+
+ url += "@";
+ }
+
+ if (isDefinedNonNullNonEmpty(this.host)) {
+ url += this.host;
+
+ if (this.port) {
+ url += ":" + this.port;
+ }
+ }
+
+ if (isDefinedNonNullNonEmpty(this.pathname)) {
+ url += this.pathname;
+ /// Trim off trailing path separators when we have a valid path
+ /// We don't do this unless pathname has elements otherwise we will trim the `://`
+ if (url.endsWith("/") && this.pathname.length > 0) {
+ url = url.slice(0, -1);
+ }
+ }
+
+ if (this.query != null && Object.keys(this.query).length > 0) {
+ url += "?" + URL.toQueryString(this.query);
+ }
+
+ if (isDefinedNonNullNonEmpty(this.hash)) {
+ url += "#" + this.hash;
+ }
+
+ return url;
+ }
+
+ // ----------------
+ // Static API
+ // ----------------
+
+ /**
+ * Converts a string into a query dictionary
+ * @param query The string to parse
+ * @returns The query dictionary containing the key-value pairs in the query string
+ */
+ static queryFromString(query: string): Query {
+ const result = {};
+
+ let parseResult = queryParamRegex.exec(query);
+ while (parseResult != null) {
+ const key = decodeURIComponent(parseResult[1]);
+ const value = decodeURIComponent(parseResult[2]);
+ result[key] = value;
+ parseResult = queryParamRegex.exec(query);
+ }
+
+ return result;
+ }
+
+ /**
+ * Converts a query dictionary into a query string
+ *
+ * @param query The query dictionary
+ * @returns {string} The string representation of the query dictionary
+ */
+ static toQueryString(query: Query) {
+ let queryString = "";
+
+ let first = true;
+ for (const key of Object.keys(query)) {
+ if (!first) {
+ queryString += "&";
+ }
+ first = false;
+
+ queryString += encodeURIComponent(key);
+
+ const value = query[key];
+ if (isDefinedNonNullNonEmpty(value) && value.length) {
+ queryString += "=" + encodeURIComponent(value);
+ }
+ }
+
+ return queryString;
+ }
+
+ /**
+ * Convenience method to instantiate a URL from a string
+ * @param url The URL string to parse
+ * @returns {URL} The new URL object representing the URL
+ */
+ static from(url: string): URL {
+ return new URL(url);
+ }
+
+ /**
+ * Convenience method to instantiate a URL from numerous (optional) components
+ * @param protocol The protocol type
+ * @param host The host name
+ * @param path The path
+ * @param query The query
+ * @param hash The hash
+ * @returns {URL} The new URL object representing the URL
+ */
+ static fromComponents(
+ protocol?: Opt<string>,
+ host?: Opt<string>,
+ path?: Opt<string>,
+ query?: Query,
+ hash?: string,
+ ): URL {
+ const url = new URL();
+ url.protocol = protocol;
+ url.host = host;
+ url.pathname = path;
+ url.query = query;
+ url.hash = hash;
+ return url;
+ }
+}