farmos
Version:
A JavaScript library for working with farmOS data structures and interacting with farmOS servers.
498 lines (460 loc) • 19.2 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var addIndex = require('ramda/src/addIndex.js');
var clone = require('ramda/src/clone.js');
var evolve = require('ramda/src/evolve.js');
var identity = require('ramda/src/identity.js');
var map = require('ramda/src/map.js');
var mapObjIndexed = require('ramda/src/mapObjIndexed.js');
var rPath = require('ramda/src/path.js');
var anyPass = require('ramda/src/anyPass.js');
var compose = require('ramda/src/compose.js');
var has = require('ramda/src/has.js');
var append = require('ramda/src/append.js');
var curry = require('ramda/src/curry.js');
var reduce = require('ramda/src/reduce.js');
var mergeWith = require('ramda/src/mergeWith.js');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var addIndex__default = /*#__PURE__*/_interopDefaultLegacy(addIndex);
var clone__default = /*#__PURE__*/_interopDefaultLegacy(clone);
var evolve__default = /*#__PURE__*/_interopDefaultLegacy(evolve);
var identity__default = /*#__PURE__*/_interopDefaultLegacy(identity);
var map__default = /*#__PURE__*/_interopDefaultLegacy(map);
var mapObjIndexed__default = /*#__PURE__*/_interopDefaultLegacy(mapObjIndexed);
var rPath__default = /*#__PURE__*/_interopDefaultLegacy(rPath);
var anyPass__default = /*#__PURE__*/_interopDefaultLegacy(anyPass);
var compose__default = /*#__PURE__*/_interopDefaultLegacy(compose);
var has__default = /*#__PURE__*/_interopDefaultLegacy(has);
var append__default = /*#__PURE__*/_interopDefaultLegacy(append);
var curry__default = /*#__PURE__*/_interopDefaultLegacy(curry);
var reduce__default = /*#__PURE__*/_interopDefaultLegacy(reduce);
var mergeWith__default = /*#__PURE__*/_interopDefaultLegacy(mergeWith);
const URIre = /^(http[s]?:\/\/)?([^/\s:#]+)?(:[0-9]+)?((?:\/?\w?)+(?:\/?[\w\-.]+[^#?\s])?)?(\??[^#?\s]+)?(#(?:\/?[\w\-$])*)?$/;
/**
* @typedef {Object} UriComponents
* @prop {?string} match - The full URI that matched the query.
* @prop {?string} scheme - The protocol, either "http://" or "https://".
* @prop {?string} domain - The domain and/or subdomain (eg, "api.example.com").
* @prop {?string} port - The port if specified (eg, ":80").
* @prop {?string} path - The relative directory path and/or file name (eg, "/foo/index.html").
* @prop {?string} query - Search params (eg, "?foo=42&bar=36").
* @prop {?string} fragment - The hash or JSON pointer (eg, "#Introduction", "#$defs/address").
*/
/**
* Parses a URI into its component strings.
* @param {string} uri
* @returns {UriComponents}
*/
function parseURI(uri) {
const groups = uri.match(URIre) || [];
const [
match, scheme, domain, port, path, query, fragment,
] = groups;
return {
match, scheme, domain, port, path, query, fragment,
};
}
const hasAny = compose__default["default"](anyPass__default["default"], map__default["default"](has__default["default"]));
/**
* @typedef {import('./reference').JsonSchema} JsonSchema
* @typedef {import('./reference').JsonSchemaDereferenced} JsonSchemaDereferenced
*/
const logicalKeywords = ['allOf', 'anyOf', 'oneOf', 'not'];
/** @type {(JsonSchema) => boolean} */
const hasLogicalKeyword = hasAny(logicalKeywords);
/** @type {(x: any) => Boolean} */
const boolOrThrow = (x) => {
if (typeof x === 'boolean') return x;
throw new Error(`Invalid schema: ${x}`);
};
/** @type {(x: any) => Boolean} */
const isObject = x => typeof x === 'object' && x !== null;
curry__default["default"]((fn, init, obj) => reduce__default["default"](
(acc, [key, val]) => fn(acc, val, key),
init,
Object.entries(obj),
));
/**
* @template D
*/
/**
/**
* @typedef {{ data: D, fulfilled: any[], rejected: any[] }} AltogetherResult
* @property {D} data
* @property {any[]} fulfilled
* @property {any[]} rejected
*/
/**
* @typedef {(promises: Promise[]) => AltogetherResult} AltogetherPartial
*/
/**
* Handles a list of promises of compatible type that will be executed in parallel.
* It wraps `Promise.allSettled()` and partitions the results based on their status,
* 'fulfilled' or 'rejected', while also applying a transform function that iterates
* through all fulfilled values and returns the cumulated result as 'data'.
* @typedef {Function} altogether
* @param {Function} transform
* @param {D} [initData=null]
* @param {Promise[]} [promises=[]]
* @returns {Promise<AltogetherPartial|AltogetherResult>}
*/
curry__default["default"]((transform, initData, promises) =>
Promise.allSettled(promises || []).then(reduce__default["default"]((all, result) => {
const { reason, value, status } = result;
if (status === 'fulfilled') {
return evolve__default["default"]({
data: d => transform(value, d),
fulfilled: append__default["default"](value),
}, all);
}
return evolve__default["default"]({
rejected: append__default["default"](reason),
}, all);
}, { data: initData || null, fulfilled: [], rejected: [] })));
/**
* @template T
* @param {(t: T, i: number) => T} transform
* @param {Array.<T>} array
* @returns {Array.<T>}
*/
const mapIndexed = addIndex__default["default"](map__default["default"]);
/**
* JSON Schema: A complete definition can probably be imported from a library.
* @typedef {Object|Boolean} JsonSchema
*/
/**
* JSON Schema Dereferenced: A JSON Schema, but w/o any $ref keywords. As such,
* it may contain circular references that cannot be serialized.
* @typedef {Object|Boolean} JsonSchemaDereferenced
*/
const trimPathRexEx = /^[/#\s]*|[/#\s]*$/g;
/** @type {(path: string) => String} */
const trimPath = path => path.replace(trimPathRexEx, '');
/**
* Resolve a schema definition from a JSON pointer reference.
* @param {JsonSchema} schema
* @param {string} pointer - A relative URI provided as the `$ref` keyword.
* @returns {JsonSchema}
*/
const getDefinition = (schema, pointer) => {
const pathSegments = trimPath(pointer).split('/');
const subschema = rPath__default["default"](pathSegments, schema);
if (subschema === undefined) return true;
return subschema;
};
/**
* Resolve the `$ref` keyword in given schema to its corresponding subschema.
* @param {JsonSchema} root - The root schema that contained the reference.
* @param {string} ref - The URI provided as the `$ref` keyword in the root
* schema or one of its subschemas.
* @param {Object} [options]
* @param {string} [options.retrievalURI] - The URI where the schema was found.
* @param {Object.<string, JsonSchemaDereferenced>} [options.knownReferences] -
* An object mapping known references to their corresponding dereferenced schemas.
* @returns {JsonSchema}
*/
const getReference = (root, ref, options = {}) => {
if (typeof ref !== 'string' || ref === '') {
const submsg = ref === '' ? '[empty string]' : ref;
throw new Error(`Invalid reference: ${submsg}`);
}
const { retrievalURI, knownReferences = {} } = options;
if (ref in knownReferences) return knownReferences[ref];
if (!isObject(root)) return boolOrThrow(root);
// The $id keyword takes precedence according to the JSON Schema spec.
const rootURI = root.$id || retrievalURI || null;
const {
scheme = '', domain = '', port = '', path = '', fragment = '',
} = parseURI(ref);
const baseURI = scheme + domain + port + path;
const baseIsRoot = rootURI === baseURI || ref === fragment;
let refRoot;
if (baseIsRoot) refRoot = root;
if (!baseIsRoot && baseURI in knownReferences) refRoot = knownReferences[baseURI];
if (refRoot === undefined) return true;
if (fragment) return getDefinition(refRoot, fragment);
return refRoot;
};
const setInPlace = (obj, path = [], val) => {
if (path.length < 1) return;
const [i, ...tail] = path;
if (!['string', 'number'].includes(typeof i)) throw new Error('Invalid path');
if (!(i in obj)) throw new Error('Path not found');
if (tail.length === 0) {
obj[i] = val; // eslint-disable-line no-param-reassign
return;
}
setInPlace(obj[i], tail, val);
};
/**
* Takes a schema which may contain the $ref keyword in it or in its subschemas,
* and returns an equivalent schema where those references have been replaced
* with the full schema document.
* @param {JsonSchema} root - The root schema to be dereferenced.
* @param {Object} [options]
* @param {string} [options.retrievalURI] - The URI where the schema was found.
* @param {string[]} [options.ignore] - A list of schemas to ignore. They will
* subsequently be referenced as `true`.
* @param {Object.<string, JsonSchema>} [options.knownReferences] - An object mapping
* known references to their corresponding schemas. They will also be dereferenced.
* @returns {JsonSchemaDereferenced}
*/
const dereference = (root, options = {}) => {
const { retrievalURI, ignore = [], knownReferences = {} } = options;
const knownRefsMap = new Map();
/** @type {(ref: string, refSchema: JsonSchema) => void} */
const setKnownRef = (ref, refSchema) => {
const appliedSchema = ignore.includes(ref) ? true : refSchema;
knownRefsMap.set(ref, appliedSchema);
};
Object.entries(knownReferences).forEach(([ref, refSchema]) => {
// We could just use setKnownRef here, but this prevents unnecessary recursion;
const schema = ignore.includes(ref) ? true : dereference(refSchema);
knownRefsMap.set(ref, schema);
});
// Set ignore refs to true to start, so they don't have to be checked in every
// call to `deref` below.
ignore.forEach((ref) => { knownRefsMap.set(ref, true); });
const baseURI = root.$id || retrievalURI || null;
const _root = clone__default["default"](root);
/** @type {(schema: JsonSchema, path?: Array.<string|number>) => JsonSchemaDereferenced} */
const deref = (schema, path = []) => {
if (!isObject(schema)) return boolOrThrow(schema);
let _schema = schema;
const set = (cb) => {
_schema = cb(_schema);
setInPlace(_root, path, _schema);
};
const derefSubschema = keyword => sub => deref(sub, [...path, keyword]);
const derefSubschemaObject = keyword => mapObjIndexed__default["default"]((sub, prop) =>
deref(sub, [...path, keyword, prop]));
const derefSubschemaArray = keyword => mapIndexed((sub, i) =>
deref(sub, [...path, keyword, i]));
const schemaTypes = {
string: identity__default["default"],
number: identity__default["default"],
integer: identity__default["default"],
object: evolve__default["default"]({
properties: derefSubschemaObject('properties'),
patternProperties: derefSubschemaObject('patternProperties'),
additionalProperties: derefSubschema('additionalProperties'),
}),
array: evolve__default["default"]({
items: derefSubschema('items'),
contains: derefSubschema('contains'),
prefixItems: derefSubschemaArray('prefixItems'),
}),
boolean: identity__default["default"],
null: identity__default["default"],
};
if ('type' in _schema && _schema.type in schemaTypes) {
set(schemaTypes[_schema.type]);
}
if (hasLogicalKeyword(_schema)) {
set(evolve__default["default"]({
allOf: derefSubschemaArray('allOf'),
anyOf: derefSubschemaArray('allOf'),
oneOf: derefSubschemaArray('allOf'),
not: derefSubschema('not'),
}));
}
if ('$ref' in _schema) {
const { $ref } = _schema;
// Anything beginning with # or /, the followed only by # or /.
const rootHashRE = /^[/#]+[/#]?$/;
const refIsRoot = rootHashRE.test($ref) || $ref === baseURI;
const refKey = refIsRoot ? baseURI : $ref;
if (knownRefsMap.has(refKey)) {
set(() => knownRefsMap.get(refKey));
} else if (refIsRoot) {
set(() => _root);
setKnownRef(baseURI, _root);
} else {
const opts = {
knownReferences: Object.fromEntries(knownRefsMap),
retrievalURI,
};
set(() => getReference(_root, $ref, opts));
set(sub => deref(sub, path));
setKnownRef($ref, _schema);
}
}
if (isObject(_schema) && '$id' in _schema) setKnownRef(_schema.$id, _schema);
return _schema;
};
return deref(_root);
};
/**
* @typedef {import('./reference').JsonSchema} JsonSchema
* @typedef {import('./reference').JsonSchemaDereferenced} JsonSchemaDereferenced
*/
/**
* Provide a dereferenced schema and get back the object corresponding to the
* "properties" keyword. A schema of type "array" will also be checked for the
* "items" keyword and any corresponding properties it has. Properties found
* under contitional keywords "allOf", "anyOf", "oneOf" and "not" will be
* merged; however, the "$ref" keyword will NOT be handled and will throw an
* error if encountered.
* @param {JsonSchemaDereferenced} schema - Must NOT contain the "$ref" keyword,
* nor subschemas containing "$ref".
* @returns {Object.<string, JsonSchemaDereferenced>}
*/
const getProperties = (schema) => {
if (!isObject(schema)) return {};
if ('$ref' in schema) {
// It is the responsibility of the caller to dereference the schema first.
const msg = `Unknown schema reference ($ref): "${schema.$ref}". `
+ 'Try dereferencing the schema before trying to access its properties or defaults.';
throw new Error(msg);
}
if ('properties' in schema) {
return schema.properties;
}
if ('items' in schema && 'properties' in schema.items) {
return schema.items.properties;
}
if (hasLogicalKeyword(schema)) {
const keyword = logicalKeywords.find(k => k in schema);
if (keyword === 'not') {
return map__default["default"](p => ({ not: p }), getProperties(schema.not));
}
return schema[keyword].reduce((props, subschema) => {
const subProps = getProperties(subschema);
const strategy = (b, a) => {
const aList = keyword in a ? a[keyword] : [a];
const bList = keyword in b ? b[keyword] : [b];
return { [keyword]: [...aList, ...bList] };
};
return mergeWith__default["default"](strategy, props, subProps);
}, {});
}
return {};
};
/**
* Provide a dereferenced schema of type 'object', and get back the subschema
* corresponding to the specified property name.
* @param {JsonSchemaDereferenced} schema - Must NOT contain the `$ref` keyword,
* nor subschemas containing `$ref`.
* @param {string} property - The name of a property under the `properties` keyword.
* @returns {JsonSchemaDereferenced}
*/
const getProperty = (schema, property) => {
if (typeof schema === 'boolean') return {};
if (typeof property !== 'string') throw new Error(`Invalid property: ${property}`);
const properties = getProperties(schema);
if (property in properties) {
return properties[property];
}
return {};
};
/**
* Provide a dereferenced schema of type 'object', and get back the subschema
* corresponding to the specified property name, or to the specified path.
* @param {JsonSchemaDereferenced} schema - Must NOT contain the `$ref` keyword,
* nor subschemas containing `$ref`.
* @param {...string|string[]} path - A property name, or array of property names.
* @returns {JsonSchemaDereferenced}
*/
const getPath = (schema, ...path) => {
if (typeof schema === 'boolean') return {};
const pathArray = path.flat();
if (pathArray.length === 0) return schema;
const [head, ...tail] = pathArray;
if (typeof head !== 'string') throw new Error(`Invalid path in subschema: ${head}`);
const subschema = getProperty(schema, head);
if (!isObject(subschema)) return {};
if (tail.length > 0) {
return getPath(subschema, tail);
}
return subschema;
};
/**
* Provide a dereferenced schema of type 'object', and get back a list of all its
* specified properties, or the properties of the subschema indicated by its path.
* @param {JsonSchemaDereferenced} schema - Must NOT contain the `$ref` keyword, nor
* subschemas containing `$ref`.
* @param {...string|string[]} [path] - A property name, or array of property names.
* @returns {string[]}
*/
const listProperties = (schema, ...path) => {
if (typeof schema === 'boolean') return [];
const subschema = path.length > 0 ? getPath(schema, path.flat()) : schema;
if ('properties' in subschema) {
return Object.keys(subschema.properties);
}
return [];
};
/**
* @typedef {import('./reference').JsonSchema} JsonSchema
* @typedef {import('./reference').JsonSchemaDereferenced} JsonSchemaDereferenced
*/
/** Transform function
* @typedef {(JsonSchemaDereferenced) => *} SchemaTransform
*/
/**
* Get the default value at a given path for a given schema.
* @param {JsonSchemaDereferenced} schema
* @param {string[]|string} [path] - A property name or array of property names.
* @param {Object} [options]
* @param {Object.<string, SchemaTransform>} [options.byType]
* @param {Object.<string, SchemaTransform>} [options.byFormat]
* @param {Object.<string, SchemaTransform>|string|boolean} [options.byProperty]
* @param {Object} [options.use]
* @returns {*}
*/
const getDefault = (schema, path = [], options = {}) => {
const subschema = getPath(schema, path);
if (!isObject(subschema)) return undefined;
if ('default' in subschema) return subschema.default;
if ('const' in subschema) return subschema.const;
// For recursive calls
/** @type {(sub: JsonSchemaDereferenced) => *} */
const getDef = sub => getDefault(sub, [], options);
/** @typedef {JsonSchemaDereferenced[]|Object.<string, JsonSchemaDereferenced>} SchemaFunctor */
/** @type {(sub: SchemaFunctor) => Array|Object} */
const mapGetDef = map__default["default"](getDef);
if (hasLogicalKeyword(subschema) && subschema.type === 'object') {
return evolve__default["default"]({
allOf: mapGetDef,
anyOf: mapGetDef,
oneOf: mapGetDef,
not: getDef,
}, subschema);
}
const { type } = subschema;
if (type === 'null') {
// This is the only case that should return null; if a default can't be
// resolved, undefined should be returned, as below.
return null;
}
const {
byType, byFormat, use,
} = options;
if (type === 'string') {
if (byFormat && 'format' in subschema && subschema.format in byFormat) {
const { [subschema.format]: transform } = byFormat;
return transform(subschema);
}
}
if (use && ['number', 'integer'].includes(type)) {
const keywords = ['minimum', 'maximum', 'multipleOf'];
const useOptions = Array.isArray(use) ? use : [use];
const kw = useOptions.find(k => k in subschema && keywords.includes(k));
if (kw !== undefined) return subschema[kw];
}
// Evaluate byType last, so options of higher specificity take precedence.
if (byType && type in byType) {
const { [type]: transform } = byType;
return transform(subschema);
}
return undefined;
};
exports.dereference = dereference;
exports.getDefault = getDefault;
exports.getDefinition = getDefinition;
exports.getPath = getPath;
exports.getProperties = getProperties;
exports.getProperty = getProperty;
exports.getReference = getReference;
exports.listProperties = listProperties;
exports.parseURI = parseURI;