Source: lib/util/config_utils.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.ConfigUtils');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.util.ArrayUtils');
  10. goog.require('shaka.util.ObjectUtils');
  11. /** @export */
  12. shaka.util.ConfigUtils = class {
  13. /**
  14. * @param {!Object} destination
  15. * @param {!Object} source
  16. * @param {!Object} template supplies default values
  17. * @param {!Object} overrides
  18. * Supplies override type checking. When the current path matches
  19. * the key in this object, each sub-value must match the type in this
  20. * object. If this contains an Object, it is used as the template.
  21. * @param {string} path to this part of the config
  22. * @return {boolean}
  23. * @export
  24. */
  25. static mergeConfigObjects(destination, source, template, overrides, path) {
  26. goog.asserts.assert(destination, 'Destination config must not be null!');
  27. // If true, override the template.
  28. const overrideTemplate = path in overrides;
  29. // If true, treat the source as a generic object to be copied without
  30. // descending more deeply.
  31. let genericObject = false;
  32. if (overrideTemplate) {
  33. genericObject = template.constructor == Object &&
  34. Object.keys(overrides).length == 0;
  35. } else {
  36. genericObject = template.constructor == Object &&
  37. Object.keys(template).length == 0;
  38. }
  39. // If true, don't validate the keys in the next level.
  40. const ignoreKeys = overrideTemplate || genericObject;
  41. let isValid = true;
  42. for (const k in source) {
  43. const subPath = path + '.' + k;
  44. const subTemplate = overrideTemplate ? overrides[path] : template[k];
  45. // The order of these checks is important.
  46. if (!ignoreKeys && !(k in template)) {
  47. shaka.log.alwaysError('Invalid config, unrecognized key ' + subPath);
  48. isValid = false;
  49. } else if (source[k] === undefined) {
  50. // An explicit 'undefined' value causes the key to be deleted from the
  51. // destination config and replaced with a default from the template if
  52. // possible.
  53. if (subTemplate === undefined || ignoreKeys) {
  54. // There is nothing in the template, so delete.
  55. delete destination[k];
  56. } else {
  57. // There is something in the template, so go back to that.
  58. destination[k] = shaka.util.ObjectUtils.cloneObject(subTemplate);
  59. }
  60. } else if (genericObject) {
  61. // Copy the fields of a generic object directly without a template and
  62. // without descending any deeper.
  63. destination[k] = source[k];
  64. } else if (subTemplate.constructor == Object &&
  65. source[k] &&
  66. source[k].constructor == Object) {
  67. // These are plain Objects with no other constructor.
  68. if (!destination[k]) {
  69. // Initialize the destination with the template so that normal
  70. // merging and type-checking can happen.
  71. destination[k] = shaka.util.ObjectUtils.cloneObject(subTemplate);
  72. }
  73. const subMergeValid = shaka.util.ConfigUtils.mergeConfigObjects(
  74. destination[k], source[k], subTemplate, overrides, subPath);
  75. isValid = isValid && subMergeValid;
  76. } else if (typeof source[k] != typeof subTemplate ||
  77. source[k] == null ||
  78. // Function constructors are not informative, and differ
  79. // between sync and async functions. So don't look at
  80. // constructor for function types.
  81. (typeof source[k] != 'function' &&
  82. source[k].constructor != subTemplate.constructor)) {
  83. // The source is the wrong type. This check allows objects to be
  84. // nulled, but does not allow null for any non-object fields.
  85. shaka.log.alwaysError('Invalid config, wrong type for ' + subPath);
  86. isValid = false;
  87. } else if (typeof template[k] == 'function' &&
  88. template[k].length != source[k].length) {
  89. shaka.log.alwaysWarn(
  90. 'Unexpected number of arguments for ' + subPath);
  91. destination[k] = source[k];
  92. } else if (Array.isArray(destination[k])) {
  93. // Make a copy of the input array, so that changes to the source object
  94. // don't immediately affect the running config.
  95. // Since everything here is very loosely-typed, use a cast to convince
  96. // the compiler we're not calling slice() on a potential ArrayBuffer,
  97. // which would break Tizen.
  98. destination[k] = /** @type {Array} */(source[k]).slice();
  99. } else {
  100. destination[k] = source[k];
  101. }
  102. }
  103. return isValid;
  104. }
  105. /**
  106. * Convert config from ('fieldName', value) format to a partial config object.
  107. *
  108. * E. g. from ('manifest.retryParameters.maxAttempts', 1) to
  109. * { manifest: { retryParameters: { maxAttempts: 1 }}}.
  110. *
  111. * @param {string} fieldName
  112. * @param {*} value
  113. * @return {!Object}
  114. * @export
  115. */
  116. static convertToConfigObject(fieldName, value) {
  117. const configObject = {};
  118. let last = configObject;
  119. let searchIndex = 0;
  120. let nameStart = 0;
  121. while (true) {
  122. const idx = fieldName.indexOf('.', searchIndex);
  123. if (idx < 0) {
  124. break;
  125. }
  126. if (idx == 0 || fieldName[idx - 1] != '\\') {
  127. const part = fieldName.substring(nameStart, idx).replace(/\\\./g, '.');
  128. last[part] = {};
  129. last = last[part];
  130. nameStart = idx + 1;
  131. }
  132. searchIndex = idx + 1;
  133. }
  134. last[fieldName.substring(nameStart).replace(/\\\./g, '.')] = value;
  135. return configObject;
  136. }
  137. /**
  138. * Reference the input parameters so the compiler doesn't remove them from
  139. * the calling function. Return whatever value is specified.
  140. *
  141. * This allows an empty or default implementation of a config callback that
  142. * still bears the complete function signature even in compiled mode.
  143. *
  144. * The caller should look something like this:
  145. *
  146. * const callback = (a, b, c, d) => {
  147. * return referenceParametersAndReturn(
  148. [a, b, c, d],
  149. a); // Can be anything, doesn't need to be one of the parameters
  150. * };
  151. *
  152. * @param {!Array<?>} parameters
  153. * @param {T} returnValue
  154. * @return {T}
  155. * @template T
  156. * @noinline
  157. */
  158. static referenceParametersAndReturn(parameters, returnValue) {
  159. return parameters && returnValue;
  160. }
  161. /**
  162. * @param {!Object} object
  163. * @param {!Object} base
  164. * @return {!Object}
  165. * @export
  166. */
  167. static getDifferenceFromConfigObjects(object, base) {
  168. const isObject = (obj) => {
  169. return obj && typeof obj === 'object' && !Array.isArray(obj);
  170. };
  171. const isArrayEmpty = (array) => {
  172. return Array.isArray(array) && array.length === 0;
  173. };
  174. const changes = (object, base) => {
  175. return Object.keys(object).reduce((acc, key) => {
  176. const value = object[key];
  177. // eslint-disable-next-line no-prototype-builtins
  178. if (!base.hasOwnProperty(key)) {
  179. acc[key] = value;
  180. } else if (value instanceof HTMLElement &&
  181. base[key] instanceof HTMLElement) {
  182. if (!value.isEqualNode(base[key])) {
  183. acc[key] = value;
  184. }
  185. } else if (isObject(value) && isObject(base[key])) {
  186. const diff = changes(value, base[key]);
  187. if (Object.keys(diff).length > 0 || !isObject(diff)) {
  188. acc[key] = diff;
  189. }
  190. } else if (Array.isArray(value) && Array.isArray(base[key])) {
  191. if (!shaka.util.ArrayUtils.hasSameElements(value, base[key])) {
  192. acc[key] = value;
  193. }
  194. } else if (Number.isNaN(value) && Number.isNaN(base[key])) {
  195. // Do nothing if both are NaN
  196. } else if (value !== base[key]) {
  197. acc[key] = value;
  198. }
  199. return acc;
  200. }, {});
  201. };
  202. const diff = changes(object, base);
  203. const removeEmpty = (obj) => {
  204. for (const key of Object.keys(obj)) {
  205. if (obj[key] instanceof HTMLElement) {
  206. // Do nothing if it's a HTMLElement
  207. } else if (isObject(obj[key]) && Object.keys(obj[key]).length === 0) {
  208. delete obj[key];
  209. } else if (isArrayEmpty(obj[key])) {
  210. delete obj[key];
  211. } else if (typeof obj[key] == 'function') {
  212. delete obj[key];
  213. } else if (isObject(obj[key])) {
  214. removeEmpty(obj[key]);
  215. if (Object.keys(obj[key]).length === 0) {
  216. delete obj[key];
  217. }
  218. }
  219. }
  220. };
  221. removeEmpty(diff);
  222. return diff;
  223. }
  224. };