domain_securityPolicy.js

import { Aggregation } from "./aggregation.js";
import { LocationPrecision } from "./locationPrecision.js";

/**
 * Get a Set from a Set or array or object, returning `null` if the set would be empty.
 *
 * @param {Object[]|Set<*>} obj the array, Set, or singleton object to get as a Set
 * @returns {Set<*>} the Set, or `null`
 * @private
 */
function setOrNull(obj) {
	let result = null;
	if (obj instanceof Set) {
		result = obj.size > 0 ? obj : null;
	} else if (Array.isArray(obj)) {
		result = obj.length > 0 ? new Set(obj) : null;
	} else if (obj) {
		result = new Set([obj]);
	}
	return result;
}

/**
 * Merge two sets.
 *
 * @param {Object[]|Set<*>} [set1] the first set
 * @param {Object[]|Set<*>} [set2] the second set
 * @returns {Set<*>} the merged Set, or `null` if neither arguments are sets or
 *                   neither argument have any values
 * @private
 */
function mergedSets(set1, set2) {
	let s1 = setOrNull(set1);
	let s2 = setOrNull(set2);
	if (s1 === null && s2 === null) {
		return null;
	} else if (s2 === null) {
		return s1;
	} else if (s1 === null) {
		return s2;
	} else {
		for (let v of s2.values()) {
			s1.add(v);
		}
		return s1;
	}
}

/**
 * An immutable set of security restrictions that can be attached to other objects, like auth tokens.
 *
 * Use the {@link module:domain~SecurityPolicyBuilder} to create instances of this class with a fluent API.
 * @alias module:domain~SecurityPolicy
 */
class SecurityPolicy {
	/**
	 * Constructor.
	 *
	 * @param {number[]|Set<number>} [nodeIds] the node IDs to restrict to, or `null` for no restriction
	 * @param {string[]|Set<string>} [sourceIds] the source ID to restrict to, or `null` for no restriction
	 * @param {module:domain~Aggregation[]|Set<module:domain~Aggregation>} [aggregations] the aggregation names to restrict to, or `null` for no restriction
	 * @param {module:domain~Aggregation} [minAggregation] if specified, a minimum aggregation level that is allowed
	 * @param {Set<module:domain~LocationPrecision>} [locationPrecisions] the location precision names to restrict to, or `null` for no restriction
	 * @param {module:domain~LocationPrecision} [minLocationPrecision] if specified, a minimum location precision that is allowed
	 * @param {Set<string>} [nodeMetadataPaths] the `SolarNodeMetadata` paths to restrict to, or `null` for no restriction
	 * @param {Set<string>} [userMetadataPaths] the `UserNodeMetadata` paths to restrict to, or `null` for no restriction
	 */
	constructor(
		nodeIds,
		sourceIds,
		aggregations,
		minAggregation,
		locationPrecisions,
		minLocationPrecision,
		nodeMetadataPaths,
		userMetadataPaths,
	) {
		this._nodeIds = setOrNull(nodeIds);
		this._sourceIds = setOrNull(sourceIds);
		this._aggregations = setOrNull(aggregations);
		this._minAggregation = minAggregation instanceof Aggregation ? minAggregation : null;
		this._locationPrecisions = setOrNull(locationPrecisions);
		this._minLocationPrecision =
			minLocationPrecision instanceof LocationPrecision ? minLocationPrecision : null;
		this._nodeMetadataPaths = setOrNull(nodeMetadataPaths);
		this._userMetadataPaths = setOrNull(userMetadataPaths);
		if (this.constructor === SecurityPolicy) {
			Object.freeze(this);
		}
	}

	/**
	 * Get the node IDs.
	 *
	 * @returns {Set<number>} the node IDs, or `null`
	 */
	get nodeIds() {
		return this._nodeIds;
	}

	/**
	 * Get the source IDs.
	 *
	 * @returns {Set<string>} the source IDs, or `null`
	 */
	get sourceIds() {
		return this._sourceIds;
	}

	/**
	 * Get the aggregations.
	 *
	 * @returns {Set<module:domain~Aggregation>} the aggregations, or `null`
	 */
	get aggregations() {
		return this._aggregations;
	}

	/**
	 * Get the location precisions.
	 *
	 * @returns {Set<module:domain~LocationPrecision>} the precisions, or `null`
	 */
	get locationPrecisions() {
		return this._locationPrecisions;
	}

	/**
	 * Get the minimum aggregation.
	 *
	 * @returns {module:domain~Aggregation} the minimum aggregation, or `null`
	 */
	get minAggregation() {
		return this._minAggregation;
	}

	/**
	 * Get the minimum location precision.
	 *
	 * @returns {module:domain~LocationPrecision} the minimum precision, or `null`
	 */
	get minLocationPrecision() {
		return this._minLocationPrecision;
	}

	/**
	 * Get the node metadata paths.
	 *
	 * @returns {Set<string>} the node metadata paths, or `null`
	 */
	get nodeMetadataPaths() {
		return this._nodeMetadataPaths;
	}

	/**
	 * Get the user metadata paths.
	 *
	 * @returns {Set<string>} the user metadata paths, or `null`
	 */
	get userMetadataPaths() {
		return this._userMetadataPaths;
	}

	/**
	 * Get this object as a standard JSON encoded string value.
	 *
	 * @return {string} the JSON encoded string
	 */
	toJsonEncoding() {
		let result = {};
		let val = this.nodeIds;
		if (val) {
			result.nodeIds = Array.from(val);
		}

		val = this.sourceIds;
		if (val) {
			result.sourceIds = Array.from(val);
		}

		val = this.aggregations;
		if (val) {
			result.aggregations = Array.from(val).map((e) => e.name);
		}

		val = this.locationPrecisions;
		if (val) {
			result.locationPrecisions = Array.from(val).map((e) => e.name);
		}

		val = this.minAggregation;
		if (val) {
			if (result.length > 0) {
				result += "&";
			}
			result.minAggregation = val.name;
		}

		val = this.minLocationPrecision;
		if (val) {
			result.minLocationPrecision = val.name;
		}

		val = this.nodeMetadataPaths;
		if (val) {
			result.nodeMetadataPaths = Array.from(val);
		}

		val = this.userMetadataPaths;
		if (val) {
			result.userMetadataPaths = Array.from(val);
		}

		return JSON.stringify(result);
	}
}

const MIN_AGGREGATION_CACHE = new Map(); // Map<string, Set<Aggregation>>
const MIN_LOCATION_PRECISION_CACHE = new Map(); // Map<string, Set<LocationPrecision>>

/**
 * A mutable builder object for {@link module:domain~SecurityPolicy} instances.
 * @alias module:domain~SecurityPolicyBuilder
 */
class SecurityPolicyBuilder {
	/**
	 * Apply all properties from another SecurityPolicy.
	 *
	 * @param {module:domain~SecurityPolicy} policy the SecurityPolicy to apply
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	withPolicy(policy) {
		if (policy) {
			this.withAggregations(policy.aggregations)
				.withMinAggregation(policy.minAggregation)
				.withLocationPrecisions(policy.locationPrecisions)
				.withMinLocationPrecision(policy.minLocationPrecision)
				.withNodeIds(policy.nodeIds)
				.withSourceIds(policy.sourceIds)
				.withNodeMetadataPaths(policy.nodeMetadataPaths)
				.withUserMetadataPaths(policy.userMetadataPaths);
		}
		return this;
	}

	/**
	 * Merge all properties from another SecurityPolicy.
	 *
	 * @param {module:domain~SecurityPolicy} policy the SecurityPolicy to merge
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	addPolicy(policy) {
		if (policy) {
			this.addAggregations(policy.aggregations)
				.addLocationPrecisions(policy.locationPrecisions)
				.addNodeIds(policy.nodeIds)
				.addSourceIds(policy.sourceIds)
				.addNodeMetadataPaths(policy.nodeMetadataPaths)
				.addUserMetadataPaths(policy.userMetadataPaths);
			if (policy.minAggregation) {
				this.withMinAggregation(policy.minAggregation);
			}
			if (policy.minLocationPrecision) {
				this.withMinLocationPrecision(policy.minLocationPrecision);
			}
		}
		return this;
	}

	/**
	 * Set the node IDs.
	 *
	 * @param {number[]|Set<number>} nodeIds the node IDs to use
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	withNodeIds(nodeIds) {
		this.nodeIds = setOrNull(nodeIds);
		return this;
	}

	/**
	 * Add a set of node IDs.
	 *
	 * @param {number[]|Set<number>} nodeIds the node IDs to add
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	addNodeIds(nodeIds) {
		return this.withNodeIds(mergedSets(this.nodeIds, nodeIds));
	}

	/**
	 * Set the node metadata paths.
	 *
	 * @param {string[]|Set<string>} nodeMetadataPaths the path expressions to use
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	withNodeMetadataPaths(nodeMetadataPaths) {
		this.nodeMetadataPaths = setOrNull(nodeMetadataPaths);
		return this;
	}

	/**
	 * Add a set of node metadata paths.
	 *
	 * @param {string[]|Set<string>} nodeMetadataPaths the path expressions to add
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	addNodeMetadataPaths(nodeMetadataPaths) {
		return this.withNodeMetadataPaths(mergedSets(this.nodeMetadataPaths, nodeMetadataPaths));
	}

	/**
	 * Set the user metadata paths.
	 *
	 * @param {string[]|Set<string>} userMetadataPaths the path expressions to use
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	withUserMetadataPaths(userMetadataPaths) {
		this.userMetadataPaths = setOrNull(userMetadataPaths);
		return this;
	}

	/**
	 * Add a set of user metadata paths.
	 *
	 * @param {string[]|Set<string>} userMetadataPaths the path expressions to add
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	addUserMetadataPaths(userMetadataPaths) {
		return this.withUserMetadataPaths(mergedSets(this.userMetadataPaths, userMetadataPaths));
	}

	/**
	 * Set the source IDs.
	 *
	 * @param {string[]|Set<string>} sourceIds the source IDs to use
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	withSourceIds(sourceIds) {
		this.sourceIds = setOrNull(sourceIds);
		return this;
	}

	/**
	 * Add source IDs.
	 *
	 * @param {string[]|Set<string>} sourceIds the source IDs to add
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	addSourceIds(sourceIds) {
		return this.withSourceIds(mergedSets(this.sourceIds, sourceIds));
	}

	/**
	 * Set the aggregations.
	 *
	 * @param {module:domain~Aggregation[]|Set<module:domain~Aggregation>} aggregations the aggregations to use
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	withAggregations(aggregations) {
		this.aggregations = setOrNull(aggregations);
		return this;
	}

	/**
	 * Set the aggregations.
	 *
	 * @param {module:domain~Aggregation[]|Set<module:domain~Aggregation>} aggregations the aggregations to add
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	addAggregations(aggregations) {
		return this.withAggregations(mergedSets(this.aggregations, aggregations));
	}

	/**
	 * Set the location precisions.
	 *
	 * @param {module:domain~LocationPrecision[]|Set<module:domain~LocationPrecision>} locationPrecisions the precisions to use
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	withLocationPrecisions(locationPrecisions) {
		this.locationPrecisions = setOrNull(locationPrecisions);
		return this;
	}

	/**
	 * Add location precisions.
	 *
	 * @param {module:domain~LocationPrecision[]|Set<module:domain~LocationPrecision>} locationPrecisions the precisions to add
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	addLocationPrecisions(locationPrecisions) {
		return this.withLocationPrecisions(mergedSets(this.locationPrecisions, locationPrecisions));
	}

	/**
	 * Set a minimum aggregation level.
	 *
	 * @param {module:domain~Aggregation} minAggregation the minimum aggregation level to set
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	withMinAggregation(minAggregation) {
		this.minAggregation = minAggregation;
		return this;
	}

	/**
	 * Build the effective aggregation level set from the policy settings.
	 *
	 * This computes a set of aggregation levels based on the configured `minAggregation`
	 * and `aggregations` values.
	 *
	 * @returns {Set<module:domain~Aggregation>} the aggregation set
	 * @private
	 */
	buildAggregations() {
		const minAggregation = this.minAggregation;
		const aggregations = this.aggregations;
		if (!minAggregation && aggregations && aggregations.size > 0) {
			return aggregations;
		} else if (!minAggregation) {
			return null;
		}
		return Aggregation.minimumEnumSet(minAggregation, MIN_AGGREGATION_CACHE);
	}

	/**
	 * Treat the configured `locationPrecisions` set as a single
	 * minimum value or a list of exact values.
	 *
	 * By default if `locationPrecisions` is configured with a single
	 * value it will be treated as a <em>minimum</em> value, and any
	 * {@link module:domain~LocationPrecision} with a {@link module:domain~LocationPrecision#precision} equal
	 * to or higher than that value's level will be included in the generated
	 * {@link module:domain~SecurityPolicy#locationPrecisions} set. Set this to
	 * `null` to disable that behavior and treat
	 * `locationPrecisions` as the exact values to include in the
	 * generated {@link module:domain~SecurityPolicy#locationPrecisions} set.
	 *
	 * @param {module:domain~LocationPrecision|null} minLocationPrecision
	 *        `null` to treat configured location precision values
	 *        as-is, or else the minimum threshold
	 * @returns {module:domain~SecurityPolicyBuilder} this object
	 */
	withMinLocationPrecision(minLocationPrecision) {
		this.minLocationPrecision = minLocationPrecision;
		return this;
	}

	/**
	 * Build the effective aggregation level set from the policy settings.
	 *
	 * This computes a set of location precision levels based on the configured `minLocationPrecision`
	 * and `locationPrecisions` values.
	 *
	 * @returns {Set<module:domain~LocationPrecision>} the precision set
	 * @private
	 */
	buildLocationPrecisions() {
		const minLocationPrecision = this.minLocationPrecision;
		const locationPrecisions = this.locationPrecisions;
		if (!minLocationPrecision && locationPrecisions && locationPrecisions.size > 0) {
			return locationPrecisions;
		} else if (!minLocationPrecision) {
			return null;
		}
		return LocationPrecision.minimumEnumSet(minLocationPrecision, MIN_LOCATION_PRECISION_CACHE);
	}

	/**
	 * Create a new {@link SecurityPolicy} out of the properties configured on this builder.
	 *
	 * @returns {module:domain~SecurityPolicy} the new policy instance
	 */
	build() {
		return new SecurityPolicy(
			this.nodeIds,
			this.sourceIds,
			this.buildAggregations(),
			this.minAggregation,
			this.buildLocationPrecisions(),
			this.minLocationPrecision,
			this.nodeMetadataPaths,
			this.userMetadataPaths,
		);
	}
}

export default SecurityPolicy;
export { SecurityPolicyBuilder };