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