index.cjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. 'use strict';
  2. /**
  3. * @fileoverview Merge Strategy
  4. */
  5. //-----------------------------------------------------------------------------
  6. // Class
  7. //-----------------------------------------------------------------------------
  8. /**
  9. * Container class for several different merge strategies.
  10. */
  11. class MergeStrategy {
  12. /**
  13. * Merges two keys by overwriting the first with the second.
  14. * @param {*} value1 The value from the first object key.
  15. * @param {*} value2 The value from the second object key.
  16. * @returns {*} The second value.
  17. */
  18. static overwrite(value1, value2) {
  19. return value2;
  20. }
  21. /**
  22. * Merges two keys by replacing the first with the second only if the
  23. * second is defined.
  24. * @param {*} value1 The value from the first object key.
  25. * @param {*} value2 The value from the second object key.
  26. * @returns {*} The second value if it is defined.
  27. */
  28. static replace(value1, value2) {
  29. if (typeof value2 !== "undefined") {
  30. return value2;
  31. }
  32. return value1;
  33. }
  34. /**
  35. * Merges two properties by assigning properties from the second to the first.
  36. * @param {*} value1 The value from the first object key.
  37. * @param {*} value2 The value from the second object key.
  38. * @returns {*} A new object containing properties from both value1 and
  39. * value2.
  40. */
  41. static assign(value1, value2) {
  42. return Object.assign({}, value1, value2);
  43. }
  44. }
  45. /**
  46. * @fileoverview Validation Strategy
  47. */
  48. //-----------------------------------------------------------------------------
  49. // Class
  50. //-----------------------------------------------------------------------------
  51. /**
  52. * Container class for several different validation strategies.
  53. */
  54. class ValidationStrategy {
  55. /**
  56. * Validates that a value is an array.
  57. * @param {*} value The value to validate.
  58. * @returns {void}
  59. * @throws {TypeError} If the value is invalid.
  60. */
  61. static array(value) {
  62. if (!Array.isArray(value)) {
  63. throw new TypeError("Expected an array.");
  64. }
  65. }
  66. /**
  67. * Validates that a value is a boolean.
  68. * @param {*} value The value to validate.
  69. * @returns {void}
  70. * @throws {TypeError} If the value is invalid.
  71. */
  72. static boolean(value) {
  73. if (typeof value !== "boolean") {
  74. throw new TypeError("Expected a Boolean.");
  75. }
  76. }
  77. /**
  78. * Validates that a value is a number.
  79. * @param {*} value The value to validate.
  80. * @returns {void}
  81. * @throws {TypeError} If the value is invalid.
  82. */
  83. static number(value) {
  84. if (typeof value !== "number") {
  85. throw new TypeError("Expected a number.");
  86. }
  87. }
  88. /**
  89. * Validates that a value is a object.
  90. * @param {*} value The value to validate.
  91. * @returns {void}
  92. * @throws {TypeError} If the value is invalid.
  93. */
  94. static object(value) {
  95. if (!value || typeof value !== "object") {
  96. throw new TypeError("Expected an object.");
  97. }
  98. }
  99. /**
  100. * Validates that a value is a object or null.
  101. * @param {*} value The value to validate.
  102. * @returns {void}
  103. * @throws {TypeError} If the value is invalid.
  104. */
  105. static "object?"(value) {
  106. if (typeof value !== "object") {
  107. throw new TypeError("Expected an object or null.");
  108. }
  109. }
  110. /**
  111. * Validates that a value is a string.
  112. * @param {*} value The value to validate.
  113. * @returns {void}
  114. * @throws {TypeError} If the value is invalid.
  115. */
  116. static string(value) {
  117. if (typeof value !== "string") {
  118. throw new TypeError("Expected a string.");
  119. }
  120. }
  121. /**
  122. * Validates that a value is a non-empty string.
  123. * @param {*} value The value to validate.
  124. * @returns {void}
  125. * @throws {TypeError} If the value is invalid.
  126. */
  127. static "string!"(value) {
  128. if (typeof value !== "string" || value.length === 0) {
  129. throw new TypeError("Expected a non-empty string.");
  130. }
  131. }
  132. }
  133. /**
  134. * @fileoverview Object Schema
  135. */
  136. //-----------------------------------------------------------------------------
  137. // Types
  138. //-----------------------------------------------------------------------------
  139. /** @import * as $typests from "./types.ts"; */
  140. /** @typedef {$typests.ObjectDefinition} ObjectDefinition */
  141. /** @typedef {$typests.PropertyDefinition} PropertyDefinition */
  142. //-----------------------------------------------------------------------------
  143. // Private
  144. //-----------------------------------------------------------------------------
  145. /**
  146. * Validates a schema strategy.
  147. * @param {string} name The name of the key this strategy is for.
  148. * @param {PropertyDefinition} definition The strategy for the object key.
  149. * @returns {void}
  150. * @throws {TypeError} When the strategy is missing a name.
  151. * @throws {TypeError} When the strategy is missing a merge() method.
  152. * @throws {TypeError} When the strategy is missing a validate() method.
  153. */
  154. function validateDefinition(name, definition) {
  155. let hasSchema = false;
  156. if (definition.schema) {
  157. if (typeof definition.schema === "object") {
  158. hasSchema = true;
  159. } else {
  160. throw new TypeError("Schema must be an object.");
  161. }
  162. }
  163. if (typeof definition.merge === "string") {
  164. if (!(definition.merge in MergeStrategy)) {
  165. throw new TypeError(
  166. `Definition for key "${name}" missing valid merge strategy.`,
  167. );
  168. }
  169. } else if (!hasSchema && typeof definition.merge !== "function") {
  170. throw new TypeError(
  171. `Definition for key "${name}" must have a merge property.`,
  172. );
  173. }
  174. if (typeof definition.validate === "string") {
  175. if (!(definition.validate in ValidationStrategy)) {
  176. throw new TypeError(
  177. `Definition for key "${name}" missing valid validation strategy.`,
  178. );
  179. }
  180. } else if (!hasSchema && typeof definition.validate !== "function") {
  181. throw new TypeError(
  182. `Definition for key "${name}" must have a validate() method.`,
  183. );
  184. }
  185. }
  186. //-----------------------------------------------------------------------------
  187. // Errors
  188. //-----------------------------------------------------------------------------
  189. /**
  190. * Error when an unexpected key is found.
  191. */
  192. class UnexpectedKeyError extends Error {
  193. /**
  194. * Creates a new instance.
  195. * @param {string} key The key that was unexpected.
  196. */
  197. constructor(key) {
  198. super(`Unexpected key "${key}" found.`);
  199. }
  200. }
  201. /**
  202. * Error when a required key is missing.
  203. */
  204. class MissingKeyError extends Error {
  205. /**
  206. * Creates a new instance.
  207. * @param {string} key The key that was missing.
  208. */
  209. constructor(key) {
  210. super(`Missing required key "${key}".`);
  211. }
  212. }
  213. /**
  214. * Error when a key requires other keys that are missing.
  215. */
  216. class MissingDependentKeysError extends Error {
  217. /**
  218. * Creates a new instance.
  219. * @param {string} key The key that was unexpected.
  220. * @param {Array<string>} requiredKeys The keys that are required.
  221. */
  222. constructor(key, requiredKeys) {
  223. super(`Key "${key}" requires keys "${requiredKeys.join('", "')}".`);
  224. }
  225. }
  226. /**
  227. * Wrapper error for errors occuring during a merge or validate operation.
  228. */
  229. class WrapperError extends Error {
  230. /**
  231. * Creates a new instance.
  232. * @param {string} key The object key causing the error.
  233. * @param {Error} source The source error.
  234. */
  235. constructor(key, source) {
  236. super(`Key "${key}": ${source.message}`, { cause: source });
  237. // copy over custom properties that aren't represented
  238. for (const sourceKey of Object.keys(source)) {
  239. if (!(sourceKey in this)) {
  240. this[sourceKey] = source[sourceKey];
  241. }
  242. }
  243. }
  244. }
  245. //-----------------------------------------------------------------------------
  246. // Main
  247. //-----------------------------------------------------------------------------
  248. /**
  249. * Represents an object validation/merging schema.
  250. */
  251. class ObjectSchema {
  252. /**
  253. * Track all definitions in the schema by key.
  254. * @type {Map<string, PropertyDefinition>}
  255. */
  256. #definitions = new Map();
  257. /**
  258. * Separately track any keys that are required for faster validtion.
  259. * @type {Map<string, PropertyDefinition>}
  260. */
  261. #requiredKeys = new Map();
  262. /**
  263. * Creates a new instance.
  264. * @param {ObjectDefinition} definitions The schema definitions.
  265. * @throws {Error} When the definitions are missing or invalid.
  266. */
  267. constructor(definitions) {
  268. if (!definitions) {
  269. throw new Error("Schema definitions missing.");
  270. }
  271. // add in all strategies
  272. for (const key of Object.keys(definitions)) {
  273. validateDefinition(key, definitions[key]);
  274. // normalize merge and validate methods if subschema is present
  275. if (typeof definitions[key].schema === "object") {
  276. const schema = new ObjectSchema(definitions[key].schema);
  277. definitions[key] = {
  278. ...definitions[key],
  279. merge(first = {}, second = {}) {
  280. return schema.merge(first, second);
  281. },
  282. validate(value) {
  283. ValidationStrategy.object(value);
  284. schema.validate(value);
  285. },
  286. };
  287. }
  288. // normalize the merge method in case there's a string
  289. if (typeof definitions[key].merge === "string") {
  290. definitions[key] = {
  291. ...definitions[key],
  292. merge: MergeStrategy[
  293. /** @type {string} */ (definitions[key].merge)
  294. ],
  295. };
  296. }
  297. // normalize the validate method in case there's a string
  298. if (typeof definitions[key].validate === "string") {
  299. definitions[key] = {
  300. ...definitions[key],
  301. validate:
  302. ValidationStrategy[
  303. /** @type {string} */ (definitions[key].validate)
  304. ],
  305. };
  306. }
  307. this.#definitions.set(key, definitions[key]);
  308. if (definitions[key].required) {
  309. this.#requiredKeys.set(key, definitions[key]);
  310. }
  311. }
  312. }
  313. /**
  314. * Determines if a strategy has been registered for the given object key.
  315. * @param {string} key The object key to find a strategy for.
  316. * @returns {boolean} True if the key has a strategy registered, false if not.
  317. */
  318. hasKey(key) {
  319. return this.#definitions.has(key);
  320. }
  321. /**
  322. * Merges objects together to create a new object comprised of the keys
  323. * of the all objects. Keys are merged based on the each key's merge
  324. * strategy.
  325. * @param {...Object} objects The objects to merge.
  326. * @returns {Object} A new object with a mix of all objects' keys.
  327. * @throws {TypeError} If any object is invalid.
  328. */
  329. merge(...objects) {
  330. // double check arguments
  331. if (objects.length < 2) {
  332. throw new TypeError("merge() requires at least two arguments.");
  333. }
  334. if (
  335. objects.some(
  336. object => object === null || typeof object !== "object",
  337. )
  338. ) {
  339. throw new TypeError("All arguments must be objects.");
  340. }
  341. return objects.reduce((result, object) => {
  342. this.validate(object);
  343. for (const [key, strategy] of this.#definitions) {
  344. try {
  345. if (key in result || key in object) {
  346. const merge = /** @type {Function} */ (strategy.merge);
  347. const value = merge.call(
  348. this,
  349. result[key],
  350. object[key],
  351. );
  352. if (value !== undefined) {
  353. result[key] = value;
  354. }
  355. }
  356. } catch (ex) {
  357. throw new WrapperError(key, ex);
  358. }
  359. }
  360. return result;
  361. }, {});
  362. }
  363. /**
  364. * Validates an object's keys based on the validate strategy for each key.
  365. * @param {Object} object The object to validate.
  366. * @returns {void}
  367. * @throws {Error} When the object is invalid.
  368. */
  369. validate(object) {
  370. // check existing keys first
  371. for (const key of Object.keys(object)) {
  372. // check to see if the key is defined
  373. if (!this.hasKey(key)) {
  374. throw new UnexpectedKeyError(key);
  375. }
  376. // validate existing keys
  377. const definition = this.#definitions.get(key);
  378. // first check to see if any other keys are required
  379. if (Array.isArray(definition.requires)) {
  380. if (
  381. !definition.requires.every(otherKey => otherKey in object)
  382. ) {
  383. throw new MissingDependentKeysError(
  384. key,
  385. definition.requires,
  386. );
  387. }
  388. }
  389. // now apply remaining validation strategy
  390. try {
  391. const validate = /** @type {Function} */ (definition.validate);
  392. validate.call(definition, object[key]);
  393. } catch (ex) {
  394. throw new WrapperError(key, ex);
  395. }
  396. }
  397. // ensure required keys aren't missing
  398. for (const [key] of this.#requiredKeys) {
  399. if (!(key in object)) {
  400. throw new MissingKeyError(key);
  401. }
  402. }
  403. }
  404. }
  405. exports.MergeStrategy = MergeStrategy;
  406. exports.ObjectSchema = ObjectSchema;
  407. exports.ValidationStrategy = ValidationStrategy;