/**
* @author Michael Hasenstein <hasenstein@yahoo.com>
* @copyright REFINIO GmbH 2017
* @license CC-BY-NC-SA-2.5; portions MIT License
* @version 0.0.1
*/
/**
* Helper functions for (ONE) objects. Most of them are only useful for ONE objects, except for
* `createReadonlyTrackingObj()` which works for any Javascript object.
* @module
*/
import type {OneVersionedObjectInterfaces} from '@OneObjectInterfaces';
import {createError} from '../errors';
import {BINARY} from '../object-recipes';
import {convertObjToIdMicrodata, convertObjToMicrodata} from '../object-to-microdata';
import type {OneIdObjectTypes, OneObjectTypes, OneVersionedObjectTypes} from '../recipes';
import {createCryptoHash} from '../system/crypto-helpers';
import {substrForceMemCopy} from './string';
import type {SHA256Hash, SHA256IdHash} from './type-checks';
/**
* The generic type "Object" was deprecated, but having a simple word is much more readable than
* the complex syntax that is supposed to be used instead. That is why we create our own
* alias name.
* @private
* @typedef {object} AnyObject
*/
export type AnyObject = Record<string, any>;
/**
* A string constant containing the string any ONE microdata object would start with.
* @static
* @type {'<div itemscope itemtype="//refin.io/'}
*/
export const MICRODATA_START = '<div itemscope itemtype="//refin.io/';
/**
* ID objects have an attribute data-id-object="true" in their outer span tag. This replaces
* the beginning of the outer span tag to make ONE ID object microdata from mere ONE object
* data. The purpose of the (never written, purely virtual!) attribute is to make ID objects
* and ID hashes different from the hash of an ordinary ONE object that happens to have only
* the properties that also are ID properties, so that no (concrete) ONE object's SHA-256 is
* the same as its ID hash.
*
* A normal object:
*
* `<span itemScope itemType="//refin.io/MyType">...</span>`
*
* An ID object (purely virtual):
*
* `<div data-id-object="true" itemScope itemType="//refin.io/MyType">...</span>`
*
* @static
* @type {'data-id-object="true"'}
*/
export const ID_OBJECT_ATTR = 'data-id-object="true"';
/**
* A string constant containing the string any ONE microdata object would start with.
* @static
* @type {'<div itemscope itemtype="//refin.io/'}
*/
export const ID_OBJ_MICRODATA_START = `<div ${ID_OBJECT_ATTR} itemscope itemtype="//refin.io/`;
/**
* A helper function that takes a ONE object microdata string and returns `true` if the
* microdata represents an ID object, `false` otherwise.
* @static
* @param {string} html
* @returns {boolean}
*/
export function isIdObjMicrodata(html: string): boolean {
return html.substr(5, ID_OBJECT_ATTR.length) === ID_OBJECT_ATTR;
}
/**
* Given a ONE object in JS object notation, this function converts the object to microdata
* format and then calculates and returns the crypto-hash over that string.
* @static
* @async
* @param {OneObjectTypes} obj - A ONE object in Javascript object format (if it was microdata
* format one could calculate the hash directly).
* @returns {Promise<SHA256Hash>} Returns a promise that resolves with the SHA-256 hash
*/
export async function calculateHashOfObj<T extends OneObjectTypes>(obj: T): Promise<SHA256Hash<T>> {
// This function will do the Error throwing for us if the object is not a valid ONE object
const microdata = convertObjToMicrodata(obj);
return await createCryptoHash<T>(microdata);
}
/**
* This function takes a ONE object in Javascript object representation, converts it into an
* ID object (i.e. it only has fields defined as ID fields in object-recipes.js for the given
* type of ONE object), converts that into a microdata string, and then calculates the
* crypto-hash of that string.
* @static
* @async
* @param {(OneVersionedObjectTypes|OneIdObjectTypes)} obj - A versioned ONE object or an ID
* object for such an object
* @returns {Promise<SHA256IdHash>} ID hash of the given versioned ONE object
*/
export async function calculateIdHashOfObj<T extends OneVersionedObjectTypes | OneIdObjectTypes>(
obj: T
): Promise<SHA256IdHash<OneVersionedObjectInterfaces[T['$type$']]>> {
// This function will do the Error throwing for us if the object is not a valid ONE object
const microdata = convertObjToIdMicrodata(obj);
// NOTE: The type is so complex because this function needs to accept ID objects, but we
// don't want SHA256IdHash to use ID objects, since those objects never really exist apart
// from right here and their interface declarations would mess with the real ones and cause
// problems.
return (await createCryptoHash(microdata)) as unknown as SHA256IdHash<
// Using OneVersionedObjectInterfaces[T['$type$']] because if we used T directly it would
// include ID object interfaces, using this construct we always get the non-ID interface of
// the object type
OneVersionedObjectInterfaces[T['$type$']]
>;
}
/**
* This function extracts the ONE object type name string from the "itemtype" attribute of the
* span tag surrounding ONE object data in its microdata HTML string representation.
* The function does <i>not</i> check if the type has a known recipe in the current runtime! That
* is why the return type only is `string` and not the much stronger `OneObjectTypeNames`.
* @static
* @param {string} microdata - A ONE microdata object, or the part of it at the beginning. Only
* the full opening <span> tag up to and including its ">" closing character are needed and used.
* @returns {(string|'BLOB')} The type string of the given microdata object, the type string
* plus " [ID]" if it is an ID object, or 'BLOB' if the given string does not look like ONE
* object microdata
*/
export function getTypeFromMicrodata(microdata: string): string | 'BLOB' {
const isIdObj = isIdObjMicrodata(microdata);
const MatchStr = isIdObj ? ID_OBJ_MICRODATA_START : MICRODATA_START;
if (!microdata.startsWith(MatchStr)) {
return BINARY;
}
// Extracts the TYPE string from the opening span tag:
// <div itemscope itemtype="//refin.io/TYPE">
const type = substrForceMemCopy(
microdata,
MatchStr.length,
microdata.indexOf('">', MatchStr.length) - MatchStr.length
);
// This is not a core responsibility of this function, but if this happens then something is
// wrong with this microdata, and we report it anyway even if it isn't our job.
if (type === '') {
throw createError('UO-TFM1', {microdata});
}
return isIdObj ? type + ' [ID]' : type;
}
/**
* A Set object containing the strings 'boolean', 'number', 'string', 'symbol'.
*
* These are pure value types, i.e. unlike a reference value pointing to an object guarding just
* those values is sufficient to protect them from being mutated.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures}
* @private
* @typedef {Set<string>} BASIC_TYPES
*/
const BASIC_TYPES = new Set(['boolean', 'number', 'string', 'symbol']);
/**
* Creates a read-only object tracking the top-level properties of another object. The use case
* is if you need an object that is read-write in one context but read-only in another. This is
* not possible, but we can create an object with no setters and with getters that deliver the
* values of the respective property of the original object, as a real-time and read-only copy
* of the object.
* @static
* @param {object} obj - An object whose top-level enumerable properties will be made available
* through a new read-only object
* @param {boolean} [includeMutableReferences=false] - Only the basic types `number`, `boolean` and
* `string` can be tracked read-only, because objects are reference values. By default, we leave
* them out of the tracking object because we cannot guarantee the read-only status.
* @returns {object} A read-only object whose top-level properties track the values of the
* top-level properties of the given object
*/
export function createReadonlyTrackingObj<T extends AnyObject>(
obj: Readonly<T>,
includeMutableReferences: boolean = false
): Readonly<T> {
return Object.create(
Object.prototype,
Object.keys(obj).reduce((conf, key) => {
(conf as AnyObject)[key] = {
enumerable: true,
configurable: false,
get: () => {
if (BASIC_TYPES.has(typeof obj[key]) || includeMutableReferences) {
return obj[key];
}
throw createError('UO-TRACKP1', {key});
},
set: () => {
throw createError('UO-TRACKP2', {key});
}
};
return conf;
}, {} as Readonly<T>)
);
}