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 {
  93. destination[k] = source[k];
  94. }
  95. }
  96. return isValid;
  97. }
  98. /**
  99. * Convert config from ('fieldName', value) format to a partial config object.
  100. *
  101. * E. g. from ('manifest.retryParameters.maxAttempts', 1) to
  102. * { manifest: { retryParameters: { maxAttempts: 1 }}}.
  103. *
  104. * @param {string} fieldName
  105. * @param {*} value
  106. * @return {!Object}
  107. * @export
  108. */
  109. static convertToConfigObject(fieldName, value) {
  110. const configObject = {};
  111. let last = configObject;
  112. let searchIndex = 0;
  113. let nameStart = 0;
  114. while (true) {
  115. const idx = fieldName.indexOf('.', searchIndex);
  116. if (idx < 0) {
  117. break;
  118. }
  119. if (idx == 0 || fieldName[idx - 1] != '\\') {
  120. const part = fieldName.substring(nameStart, idx).replace(/\\\./g, '.');
  121. last[part] = {};
  122. last = last[part];
  123. nameStart = idx + 1;
  124. }
  125. searchIndex = idx + 1;
  126. }
  127. last[fieldName.substring(nameStart).replace(/\\\./g, '.')] = value;
  128. return configObject;
  129. }
  130. /**
  131. * Reference the input parameters so the compiler doesn't remove them from
  132. * the calling function. Return whatever value is specified.
  133. *
  134. * This allows an empty or default implementation of a config callback that
  135. * still bears the complete function signature even in compiled mode.
  136. *
  137. * The caller should look something like this:
  138. *
  139. * const callback = (a, b, c, d) => {
  140. * return referenceParametersAndReturn(
  141. [a, b, c, d],
  142. a); // Can be anything, doesn't need to be one of the parameters
  143. * };
  144. *
  145. * @param {!Array<?>} parameters
  146. * @param {T} returnValue
  147. * @return {T}
  148. * @template T
  149. * @noinline
  150. */
  151. static referenceParametersAndReturn(parameters, returnValue) {
  152. return parameters && returnValue;
  153. }
  154. /**
  155. * @param {!Object} object
  156. * @param {!Object} base
  157. * @return {!Object}
  158. * @export
  159. */
  160. static getDifferenceFromConfigObjects(object, base) {
  161. const isObject = (obj) => {
  162. return obj && typeof obj === 'object' && !Array.isArray(obj);
  163. };
  164. const isArrayEmpty = (array) => {
  165. return Array.isArray(array) && array.length === 0;
  166. };
  167. const changes = (object, base) => {
  168. return Object.keys(object).reduce((acc, key) => {
  169. const value = object[key];
  170. // eslint-disable-next-line no-prototype-builtins
  171. if (!base.hasOwnProperty(key)) {
  172. acc[key] = value;
  173. } else if (value instanceof HTMLElement &&
  174. base[key] instanceof HTMLElement) {
  175. if (!value.isEqualNode(base[key])) {
  176. acc[key] = value;
  177. }
  178. } else if (isObject(value) && isObject(base[key])) {
  179. const diff = changes(value, base[key]);
  180. if (Object.keys(diff).length > 0 || !isObject(diff)) {
  181. acc[key] = diff;
  182. }
  183. } else if (Array.isArray(value) && Array.isArray(base[key])) {
  184. if (!shaka.util.ArrayUtils.hasSameElements(value, base[key])) {
  185. acc[key] = value;
  186. }
  187. } else if (Number.isNaN(value) && Number.isNaN(base[key])) {
  188. // Do nothing if both are NaN
  189. } else if (value !== base[key]) {
  190. acc[key] = value;
  191. }
  192. return acc;
  193. }, {});
  194. };
  195. const diff = changes(object, base);
  196. const removeEmpty = (obj) => {
  197. for (const key of Object.keys(obj)) {
  198. if (obj[key] instanceof HTMLElement) {
  199. // Do nothing if it's a HTMLElement
  200. } else if (isObject(obj[key]) && Object.keys(obj[key]).length === 0) {
  201. delete obj[key];
  202. } else if (isArrayEmpty(obj[key])) {
  203. delete obj[key];
  204. } else if (typeof obj[key] == 'function') {
  205. delete obj[key];
  206. } else if (isObject(obj[key])) {
  207. removeEmpty(obj[key]);
  208. if (Object.keys(obj[key]).length === 0) {
  209. delete obj[key];
  210. }
  211. }
  212. }
  213. };
  214. removeEmpty(diff);
  215. return diff;
  216. }
  217. };