用工小程序前端代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

301 lines
9.4 KiB

7 months ago
  1. /**
  2. * @filedescription Object Schema
  3. */
  4. "use strict";
  5. //-----------------------------------------------------------------------------
  6. // Requirements
  7. //-----------------------------------------------------------------------------
  8. const { MergeStrategy } = require("./merge-strategy");
  9. const { ValidationStrategy } = require("./validation-strategy");
  10. //-----------------------------------------------------------------------------
  11. // Private
  12. //-----------------------------------------------------------------------------
  13. const strategies = Symbol("strategies");
  14. const requiredKeys = Symbol("requiredKeys");
  15. /**
  16. * Validates a schema strategy.
  17. * @param {string} name The name of the key this strategy is for.
  18. * @param {Object} strategy The strategy for the object key.
  19. * @param {boolean} [strategy.required=true] Whether the key is required.
  20. * @param {string[]} [strategy.requires] Other keys that are required when
  21. * this key is present.
  22. * @param {Function} strategy.merge A method to call when merging two objects
  23. * with the same key.
  24. * @param {Function} strategy.validate A method to call when validating an
  25. * object with the key.
  26. * @returns {void}
  27. * @throws {Error} When the strategy is missing a name.
  28. * @throws {Error} When the strategy is missing a merge() method.
  29. * @throws {Error} When the strategy is missing a validate() method.
  30. */
  31. function validateDefinition(name, strategy) {
  32. let hasSchema = false;
  33. if (strategy.schema) {
  34. if (typeof strategy.schema === "object") {
  35. hasSchema = true;
  36. } else {
  37. throw new TypeError("Schema must be an object.");
  38. }
  39. }
  40. if (typeof strategy.merge === "string") {
  41. if (!(strategy.merge in MergeStrategy)) {
  42. throw new TypeError(`Definition for key "${name}" missing valid merge strategy.`);
  43. }
  44. } else if (!hasSchema && typeof strategy.merge !== "function") {
  45. throw new TypeError(`Definition for key "${name}" must have a merge property.`);
  46. }
  47. if (typeof strategy.validate === "string") {
  48. if (!(strategy.validate in ValidationStrategy)) {
  49. throw new TypeError(`Definition for key "${name}" missing valid validation strategy.`);
  50. }
  51. } else if (!hasSchema && typeof strategy.validate !== "function") {
  52. throw new TypeError(`Definition for key "${name}" must have a validate() method.`);
  53. }
  54. }
  55. //-----------------------------------------------------------------------------
  56. // Errors
  57. //-----------------------------------------------------------------------------
  58. /**
  59. * Error when an unexpected key is found.
  60. */
  61. class UnexpectedKeyError extends Error {
  62. /**
  63. * Creates a new instance.
  64. * @param {string} key The key that was unexpected.
  65. */
  66. constructor(key) {
  67. super(`Unexpected key "${key}" found.`);
  68. }
  69. }
  70. /**
  71. * Error when a required key is missing.
  72. */
  73. class MissingKeyError extends Error {
  74. /**
  75. * Creates a new instance.
  76. * @param {string} key The key that was missing.
  77. */
  78. constructor(key) {
  79. super(`Missing required key "${key}".`);
  80. }
  81. }
  82. /**
  83. * Error when a key requires other keys that are missing.
  84. */
  85. class MissingDependentKeysError extends Error {
  86. /**
  87. * Creates a new instance.
  88. * @param {string} key The key that was unexpected.
  89. * @param {Array<string>} requiredKeys The keys that are required.
  90. */
  91. constructor(key, requiredKeys) {
  92. super(`Key "${key}" requires keys "${requiredKeys.join("\", \"")}".`);
  93. }
  94. }
  95. /**
  96. * Wrapper error for errors occuring during a merge or validate operation.
  97. */
  98. class WrapperError extends Error {
  99. /**
  100. * Creates a new instance.
  101. * @param {string} key The object key causing the error.
  102. * @param {Error} source The source error.
  103. */
  104. constructor(key, source) {
  105. super(`Key "${key}": ${source.message}`, { cause: source });
  106. // copy over custom properties that aren't represented
  107. for (const key of Object.keys(source)) {
  108. if (!(key in this)) {
  109. this[key] = source[key];
  110. }
  111. }
  112. }
  113. }
  114. //-----------------------------------------------------------------------------
  115. // Main
  116. //-----------------------------------------------------------------------------
  117. /**
  118. * Represents an object validation/merging schema.
  119. */
  120. class ObjectSchema {
  121. /**
  122. * Creates a new instance.
  123. */
  124. constructor(definitions) {
  125. if (!definitions) {
  126. throw new Error("Schema definitions missing.");
  127. }
  128. /**
  129. * Track all strategies in the schema by key.
  130. * @type {Map}
  131. * @property strategies
  132. */
  133. this[strategies] = new Map();
  134. /**
  135. * Separately track any keys that are required for faster validation.
  136. * @type {Map}
  137. * @property requiredKeys
  138. */
  139. this[requiredKeys] = new Map();
  140. // add in all strategies
  141. for (const key of Object.keys(definitions)) {
  142. validateDefinition(key, definitions[key]);
  143. // normalize merge and validate methods if subschema is present
  144. if (typeof definitions[key].schema === "object") {
  145. const schema = new ObjectSchema(definitions[key].schema);
  146. definitions[key] = {
  147. ...definitions[key],
  148. merge(first = {}, second = {}) {
  149. return schema.merge(first, second);
  150. },
  151. validate(value) {
  152. ValidationStrategy.object(value);
  153. schema.validate(value);
  154. }
  155. };
  156. }
  157. // normalize the merge method in case there's a string
  158. if (typeof definitions[key].merge === "string") {
  159. definitions[key] = {
  160. ...definitions[key],
  161. merge: MergeStrategy[definitions[key].merge]
  162. };
  163. };
  164. // normalize the validate method in case there's a string
  165. if (typeof definitions[key].validate === "string") {
  166. definitions[key] = {
  167. ...definitions[key],
  168. validate: ValidationStrategy[definitions[key].validate]
  169. };
  170. };
  171. this[strategies].set(key, definitions[key]);
  172. if (definitions[key].required) {
  173. this[requiredKeys].set(key, definitions[key]);
  174. }
  175. }
  176. }
  177. /**
  178. * Determines if a strategy has been registered for the given object key.
  179. * @param {string} key The object key to find a strategy for.
  180. * @returns {boolean} True if the key has a strategy registered, false if not.
  181. */
  182. hasKey(key) {
  183. return this[strategies].has(key);
  184. }
  185. /**
  186. * Merges objects together to create a new object comprised of the keys
  187. * of the all objects. Keys are merged based on the each key's merge
  188. * strategy.
  189. * @param {...Object} objects The objects to merge.
  190. * @returns {Object} A new object with a mix of all objects' keys.
  191. * @throws {Error} If any object is invalid.
  192. */
  193. merge(...objects) {
  194. // double check arguments
  195. if (objects.length < 2) {
  196. throw new TypeError("merge() requires at least two arguments.");
  197. }
  198. if (objects.some(object => (object == null || typeof object !== "object"))) {
  199. throw new TypeError("All arguments must be objects.");
  200. }
  201. return objects.reduce((result, object) => {
  202. this.validate(object);
  203. for (const [key, strategy] of this[strategies]) {
  204. try {
  205. if (key in result || key in object) {
  206. const value = strategy.merge.call(this, result[key], object[key]);
  207. if (value !== undefined) {
  208. result[key] = value;
  209. }
  210. }
  211. } catch (ex) {
  212. throw new WrapperError(key, ex);
  213. }
  214. }
  215. return result;
  216. }, {});
  217. }
  218. /**
  219. * Validates an object's keys based on the validate strategy for each key.
  220. * @param {Object} object The object to validate.
  221. * @returns {void}
  222. * @throws {Error} When the object is invalid.
  223. */
  224. validate(object) {
  225. // check existing keys first
  226. for (const key of Object.keys(object)) {
  227. // check to see if the key is defined
  228. if (!this.hasKey(key)) {
  229. throw new UnexpectedKeyError(key);
  230. }
  231. // validate existing keys
  232. const strategy = this[strategies].get(key);
  233. // first check to see if any other keys are required
  234. if (Array.isArray(strategy.requires)) {
  235. if (!strategy.requires.every(otherKey => otherKey in object)) {
  236. throw new MissingDependentKeysError(key, strategy.requires);
  237. }
  238. }
  239. // now apply remaining validation strategy
  240. try {
  241. strategy.validate.call(strategy, object[key]);
  242. } catch (ex) {
  243. throw new WrapperError(key, ex);
  244. }
  245. }
  246. // ensure required keys aren't missing
  247. for (const [key] of this[requiredKeys]) {
  248. if (!(key in object)) {
  249. throw new MissingKeyError(key);
  250. }
  251. }
  252. }
  253. }
  254. exports.ObjectSchema = ObjectSchema;