Source: util/type-checks.ts

  1. /**
  2. * @author Michael Hasenstein <hasenstein@yahoo.com>
  3. * @copyright REFINIO GmbH 2018
  4. * @license CC-BY-NC-SA-2.5; portions MIT License
  5. * @version 0.0.1
  6. */
  7. /**
  8. * A module with functions that perform runtime type checks. There are very simple checks such
  9. * as `isString()`, there also are complex checks.
  10. * @module
  11. */
  12. /**
  13. * (Simulated) opaque type name alias for strings that are SHA-256 hashes _pointing to
  14. * concrete objects_.
  15. *
  16. * This type has a generic parameter: A member or a union of {@link OneObjectTypes} as well
  17. * as the virtual types {@link BLOB} and {@link CLOB}.
  18. *
  19. * This generic parameter is used by and passed through all major one.core functions' type
  20. * definitions. For example, after storing an object the hashes in the object creation
  21. * result object will be tagged with the typed of the object.
  22. * @global
  23. * @typedef {string} SHA256Hash
  24. */
  25. export type SHA256Hash<T extends HashTypes = OneObjectTypes> = string & {
  26. _: 'SHA256Hash';
  27. type: T;
  28. };
  29. /**
  30. * (Simulated) opaque type name alias for strings that are SHA-256 hashes _pointing to ID
  31. * objects, i.e. to all past, present and future versions of a versioned object_.
  32. *
  33. * This type has a generic parameter: A member or a union of {@link OneObjectTypes} as well
  34. * as the virtual types {@link BLOB} and {@link CLOB}.
  35. * @global
  36. * @typedef {string} SHA256IdHash
  37. */
  38. export type SHA256IdHash<
  39. T extends OneVersionedObjectTypes | OneIdObjectTypes = OneVersionedObjectTypes
  40. > = string & {
  41. _: 'SHA256IdHash';
  42. type: T;
  43. };
  44. /**
  45. * This is a TypeScript helper type that extracts the type of the elements of certain container
  46. * types. If the container is read-only and its elements are known this results in a union type
  47. * of those elements.
  48. *
  49. * Examples:
  50. * ```
  51. * const set = new Set(['a', 'b', 'c'] as const);
  52. * // "a" | "b" | "c"
  53. * type SSS = ElementType<typeof set>;
  54. *
  55. * const arr = ['aa', 'bb', 'cc'] as const;
  56. * // // "aa" | "bb" | "cc"
  57. * type AAA = ElementType<typeof arr>;
  58. *
  59. * const map = new Map([['a', 1], ['b', 2], ['c', 42]] as const);
  60. * // 1 | 2 | 42
  61. * type MMM = ElementType<typeof map>;
  62. * ```
  63. * @global
  64. * @template T
  65. * @typedef {*} ElementType<T>
  66. */
  67. export type ElementType<T> = T extends Array<infer U>
  68. ? U
  69. : T extends Readonly<Array<infer U>>
  70. ? U
  71. : T extends Set<infer U>
  72. ? U
  73. : T extends Readonly<Set<infer U>>
  74. ? U
  75. : T extends Map<any, infer U>
  76. ? U
  77. : T extends Readonly<Map<any, infer U>>
  78. ? U
  79. : T extends Promise<infer U>
  80. ? U
  81. : T;
  82. import {createError} from '../errors';
  83. import type {
  84. ArrayValue,
  85. BagValue,
  86. HashTypes,
  87. OneIdObjectTypes,
  88. OneObjectTypes,
  89. OneVersionedObjectTypes,
  90. RecipeRule,
  91. SetValue,
  92. ValueType
  93. } from '../recipes';
  94. import type {FileCreation, SimpleReadStream} from '../storage-base-common';
  95. import {CREATION_STATUS} from '../storage-base-common';
  96. import type {AnyObject} from './object';
  97. import type {OneEventSource, OneEventSourceConsumer} from './one-event-source';
  98. import {isFunction, isObject, isString} from './type-checks-basic';
  99. /**
  100. * A regular expression that can be used to verify that a given string looks like a
  101. * cryptographic hash string used to represent ONE objects. For SHA-256 it tests if there
  102. * are 64 characters and that each one of them is between 0-9 or a-f.
  103. * @private
  104. * @type {RegExp}
  105. */
  106. const CRYPTO_HASH_RE = /^[0-9a-f]{64}$/;
  107. /**
  108. * Used to check request results. Values "new" and "exists".
  109. * @private
  110. * @type {Set<string>}
  111. */
  112. const FILE_CREATION_STATUS_VALUES = new Set(Object.values(CREATION_STATUS));
  113. /**
  114. * An alternative to `Object.keys(o).length` that is more efficient. Running a test loop
  115. * comparing the two showed less than half the time for the for-loop option. Also, `Object.keys`
  116. * creates a temporary array.
  117. * @static
  118. * @param {object} o
  119. * @returns {number}
  120. */
  121. export function countEnumerableProperties(o: AnyObject): number {
  122. let count = 0;
  123. for (const prop in o) {
  124. if (Object.prototype.hasOwnProperty.call(o, prop)) {
  125. count += 1;
  126. }
  127. }
  128. return count;
  129. }
  130. /**
  131. * Valid encodings for file streams are binary (undefined or null), "base64" and "utf8".
  132. * @static
  133. * @param {*} thing
  134. * @returns {boolean}
  135. */
  136. export function isEncoding(thing: unknown): thing is undefined | 'base64' | 'utf8' {
  137. return thing === undefined || thing === 'base64' || thing === 'utf8';
  138. }
  139. /**
  140. * ONE uses SHA-256 hashes in hexadecimal lowercase format to represent the contents of files.
  141. * This functions tests a given "thing" of any type if it is such a string.
  142. *
  143. * For the curious: Why a function?
  144. *
  145. * While one might simply test against a regular expression using myRegEx.test(thing) this
  146. * method has one more or less theoretical problem: If the thing is an object with a toString()
  147. * method that returns a matching string the test will return "true" even though the thing is an
  148. * object and not a string. For example,
  149. * ```javascript
  150. * /^s+$/.test( {toString: () => 'sss'} );
  151. * ```
  152. * will return true.
  153. * @static
  154. * @param {*} thing - An argument that can be of any type
  155. * @returns {boolean} True if the argument is a SHA-256 lowercase hexadecimal string, false if not
  156. */
  157. export function isHash<T extends HashTypes>(
  158. thing: unknown
  159. ): thing is SHA256Hash<T> | SHA256IdHash<T extends OneVersionedObjectTypes ? T : never> {
  160. return isString(thing) && CRYPTO_HASH_RE.test(thing);
  161. }
  162. /**
  163. * Non-regex version of the "isHash" function in an attempt to save a tiny bit of CPU because we
  164. * don't need a full regex check here. "Premature optimization" vs. "it's cheap and easy", and
  165. * according to reported real world runtime experience these hash checks can add up and become
  166. * significant.
  167. * @param {*} s
  168. * @returns {boolean}
  169. */
  170. export function looksLikeHash(s: unknown): boolean {
  171. return typeof s === 'string' && s.length === 64;
  172. }
  173. /**
  174. * The function returns the given value after making sure it is a SHA-256 hexadecimal string. It
  175. * throws an Error if this is not the case.
  176. * @static
  177. * @param {*} thing
  178. * @returns {SHA256Hash}
  179. * @throws {Error}
  180. */
  181. export function ensureHash<T extends HashTypes>(thing: unknown): SHA256Hash<T> {
  182. if (isHash<T>(thing)) {
  183. return thing as SHA256Hash<T>;
  184. }
  185. throw createError('UTC-EHASH', {thing});
  186. }
  187. /**
  188. * The function returns the given value after making sure it is a SHA-256 hexadecimal string. It
  189. * throws an Error if this is not the case.
  190. * @static
  191. * @param {*} thing
  192. * @returns {SHA256Hash}
  193. * @throws {Error}
  194. */
  195. export function ensureIdHash<T extends OneVersionedObjectTypes = any>(
  196. thing: unknown
  197. ): SHA256IdHash<T> {
  198. if (isHash<T>(thing)) {
  199. return thing as SHA256IdHash<T>;
  200. }
  201. throw createError('UTC-EHASH', {thing});
  202. }
  203. /**
  204. * Checks if a given object is a ONE.core {@link OneEventSourceConsumer} object by testing the
  205. * properties (duck typing).
  206. * @static
  207. * @param {*} thing
  208. * @returns {boolean}
  209. */
  210. export function isEventSourceConsumer(thing: unknown): thing is OneEventSourceConsumer<unknown> {
  211. return isObject(thing) && isFunction(thing.addListener) && isFunction(thing.removeListener);
  212. }
  213. /**
  214. * Checks if a given object is a ONE.core {@link OneEventSource} object by testing the properties
  215. * (duck typing).
  216. * @static
  217. * @param {*} thing
  218. * @returns {boolean}
  219. */
  220. export function isEventSource(thing: unknown): thing is OneEventSource<unknown> {
  221. return isObject(thing) && isEventSourceConsumer(thing.consumer);
  222. }
  223. /**
  224. * Checks if a given object is a ONE.core {@link SimpleReadStream} object by testing the
  225. * properties (duck typing).
  226. * @static
  227. * @param {*} thing
  228. * @returns {boolean}
  229. */
  230. export function isSimpleReadStream(thing: unknown): thing is SimpleReadStream {
  231. return (
  232. isObject(thing) &&
  233. isFunction(thing.pause) &&
  234. isFunction(thing.resume) &&
  235. isFunction(thing.cancel) &&
  236. thing.promise instanceof Promise &&
  237. isEncoding(thing.thing) &&
  238. isEventSourceConsumer(thing.onData)
  239. );
  240. }
  241. /**
  242. * Checks for {@link FileCreation} objects. They are used to return the result of saving BLOBs
  243. * and CLOBs to storage.
  244. * @static
  245. * @param {*} thing
  246. * @returns {boolean}
  247. */
  248. export function isFileCreationResult(thing: unknown): thing is FileCreation<any> {
  249. return (
  250. isObject(thing) &&
  251. countEnumerableProperties(thing) === 2 &&
  252. isHash(thing.hash) &&
  253. FILE_CREATION_STATUS_VALUES.has(thing.status)
  254. );
  255. }
  256. /**
  257. * @static
  258. * @param {*} thing - Data e.g. from a network connection expected to be of format SHA256Hash[]
  259. * @returns {SHA256Hash[]} Returns the data now confirmed to be of type Array of SHA256Hash
  260. * @throws {Error} Throws an Error when the given data is not an array of (only) SHA-256
  261. * hashes
  262. */
  263. export function ensureArrayOfSHA256Hash(thing: unknown): Array<SHA256Hash<HashTypes>> {
  264. if (Array.isArray(thing) && thing.every(item => isHash(item))) {
  265. return thing as Array<SHA256Hash<HashTypes>>;
  266. }
  267. throw createError('UTC-AHASH', {thing});
  268. }
  269. /**
  270. * @static
  271. * @param {RecipeRule} obj
  272. * @returns {RecipeRule}
  273. */
  274. export function ruleHasItemType(obj: RecipeRule): obj is RecipeRule & {itemtype: ValueType} {
  275. return Object.prototype.hasOwnProperty.call(obj, 'itemtype');
  276. }
  277. /**
  278. * Check if the valueType is a list type: array, bag, or set.
  279. * @static
  280. * @param {ValueType} arg
  281. * @returns {boolean}
  282. */
  283. export function isListItemType(arg: ValueType): arg is BagValue | ArrayValue | SetValue {
  284. return arg.type === 'array' || arg.type === 'bag' || arg.type === 'set';
  285. }