Skip to content

Commit

Permalink
Merge pull request #80 from balena-io/capability-support
Browse files Browse the repository at this point in the history
Add support and tests for capability specification via `provides`
  • Loading branch information
flowzone-app[bot] authored Feb 5, 2025
2 parents 6e136bc + a79b696 commit 6524517
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 159 deletions.
3 changes: 2 additions & 1 deletion lib/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export default class Blueprint extends Contract {
(accumulator: any, value, type) => {
const selector = {
cardinality: parse(value.cardinality || value) as any,
filter: value.filter,
// Array has its own `filter` function, which we need to ignore
filter: Array.isArray(value) ? undefined : value.filter,
type: value.type || type,
version: value.version,
};
Expand Down
274 changes: 116 additions & 158 deletions lib/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,8 +648,8 @@ export default class Contract {
// the list of hashes we should check against.
const match = matches(omit(matcher.raw.data, ['slug', 'version']));
const versionMatch = matcher.raw.data.version;
if (contract.raw.capabilities) {
for (const capability of contract.raw.capabilities) {
if (contract.raw.provides) {
for (const capability of contract.raw.provides) {
if (match(capability)) {
if (versionMatch) {
if (valid(capability.version) && validRange(versionMatch)) {
Expand Down Expand Up @@ -1032,16 +1032,105 @@ export default class Contract {
[] as Contract[],
);
}

private isRequirementSatisfied(
requirement: Contract,
options: { types?: Set<string> } = {},
): boolean {
// Utilities
const shouldEvaluateType = (type: string) =>
options.types ? options.types.has(type) : true;

/**
* @summary Check if a matcher is satisfied
* @function
* @public
*
* @param {Object} matcher - matcher contract
* @returns {Boolean} whether the matcher is satisfied
*
* @example
* const matcher = Contract.createMatcher({
* type: 'sw.os',
* slug: 'debian'
* })
*
* if (hasMatch(matcher)) {
* console.log('This matcher is satisfied!')
* }
*/
const hasMatch = (matcher: Contract): boolean => {
// TODO: Write a function similar to findContracts
// that stops as soon as it finds one match
return (
this.findChildren(matcher).length > 0 ||
this.findChildrenWithCapabilities(matcher).length > 0
);
};

if (requirement.raw.operation === 'or') {
// (3.1) Note that we should only consider disjuncts
// of types we are allowed to check. We can make
// such transformation here, so we can then consider
// the disjunction as fulfilled if there are no
// remaining disjuncts.
const disjuncts = filter(requirement.raw.data.getAll(), (disjunct) => {
return shouldEvaluateType(disjunct.raw.data.type);
});
// (3.2) An empty disjuction means that this particular
// requirement is fulfilled, so we can carry on.
// A disjunction naturally contains a list of further
// requirements we need to check for. If at least one
// of the members is fulfilled, we can proceed with
// next requirement.
if (disjuncts.length === 0 || some(disjuncts, hasMatch)) {
return true;
}
// (3.3) If no members were fulfilled, then we know
// that this requirement was not fullfilled, so it will be returned
return false;
} else if (requirement.raw.operation === 'not') {
// (3.4) Note that we should only consider disjuncts
// of types we are allowed to check. We can make
// such transformation here, so we can then consider
// the disjunction as fulfilled if there are no
// remaining disjuncts.
// (3.5) We fail the requirement if the set of negated
// disjuncts is not empty, and we have at least one of
// them in the context.
if (
some(requirement.raw.data.getAll(), (disjunct) => {
return (
shouldEvaluateType(disjunct.raw.data.type) && hasMatch(disjunct)
);
})
) {
return false;
}
return true;
}
// (4) If we should evaluate this requirement and it is not fullfilled
// it will be returned
if (
shouldEvaluateType(requirement.raw.data.type) &&
!hasMatch(requirement)
) {
return false;
}

return true;
}

/**
* @summary Check if a child contract is satisfied when applied to this contract
* @summary Get a list of child requirements that are not satisfied by this contract
* @function
* @name module:contrato.Contract#satisfiesChildContract
* @public
*
* @param {Object} contract - child contract
* @param {Object} [options] - options
* @param {Set} [options.types] - the types to consider (all by default)
* @returns {Boolean} whether the contract is satisfied
* @returns list of unsatisfied requirements
*
* @example
* const contract = new Contract({ ... })
Expand Down Expand Up @@ -1087,96 +1176,33 @@ export default class Contract {
*/
getNotSatisfiedChildRequirements(
contract: Contract,
options: { types: Set<string> } = { types: new Set() },
): any[] {
const conjuncts = reduce(
contract.getChildren(),
(accumulator, child) => {
return accumulator.concat(
child.metadata.requirements.compiled.getAll(),
);
},
contract.metadata.requirements.compiled.getAll(),
);
options: { types?: Set<string> } = {},
) {
const conjuncts: Contract[] = contract.metadata.requirements.compiled
.getAll()
.concat(
contract
.getChildren()
.flatMap((child) => child.metadata.requirements.compiled.getAll()),
);
// (1) If the top level list of conjuncts is empty,
// then we can assume the requirements are fulfilled
// and stop without doing any further computations.
if (conjuncts.length === 0) {
return [];
}
// Utilities
const shouldEvaluateType = (type: string) =>
options.types ? options.types.has(type) : true;

const requirements: any[] = [];
/**
* @summary Check if a matcher is satisfied
* @function
* @public
*
* @param {Object} matcher - matcher contract
* @returns {Boolean} whether the matcher is satisfied
*
* @example
* const matcher = Contract.createMatcher({
* type: 'sw.os',
* slug: 'debian'
* })
*
* if (hasMatch(matcher)) {
* console.log('This matcher is satisfied!')
* }
*/
const hasMatch = (matcher: Contract): boolean => {
// TODO: Write a function similar to findContracts
// that stops as soon as it finds one match
return (
this.findChildren(matcher).length > 0 ||
this.findChildrenWithCapabilities(matcher).length > 0
);
};
// (2) The requirements are specified as a list of objects,
// so lets iterate through those.
// This function uses a for loop instead of a more functional
// construct for performance reasons, given that we can freely
// break out of the loop as soon as possible.
for (const conjunct of conjuncts) {
if (conjunct.raw.operation === 'or') {
// (3.1) Note that we should only consider disjuncts
// of types we are allowed to check. We can make
// such transformation here, so we can then consider
// the disjunction as fulfilled if there are no
// remaining disjuncts.
const disjuncts = filter(conjunct.raw.data.getAll(), (disjunct) => {
return shouldEvaluateType(disjunct.raw.data.type);
});
// (3.2) An empty disjuction means that this particular
// requirement is fulfilled, so we can carry on.
// A disjunction naturally contains a list of further
// requirements we need to check for. If at least one
// of the members is fulfilled, we can proceed with
// next requirement.
if (disjuncts.length === 0 || some(map(disjuncts, hasMatch))) {
continue;
}
// (3.3) If no members were fulfilled, then we know
// that this requirement was not fullfilled, so it will be returned
requirements.push(conjunct.raw.data);
}
// (4) If we should evaluate this requirement and it is not fullfilled
// it will be returned
if (shouldEvaluateType(conjunct.raw.data.type) && !hasMatch(conjunct)) {
requirements.push(conjunct.raw.data);
} else if (!shouldEvaluateType(conjunct.raw.data.type)) {
// If this requirement is not evaluated, because of missing contracts,
// it will also be returned.
requirements.push(conjunct.raw.data);
}
}
return conjuncts
.filter((conjunct) => !this.isRequirementSatisfied(conjunct, options))
.map((conjunct) => conjunct.raw.data);
// (5) If we reached this far, then it means that all the
// requirements were checked, and they were all satisfied,
// so this is good to go!
return requirements;
}
/**
* @summary Check if a child contract is satisfied when applied to this contract
Expand Down Expand Up @@ -1235,97 +1261,29 @@ export default class Contract {
contract: Contract,
options: { types?: Set<string> } = {},
): boolean {
const conjuncts = reduce(
contract.getChildren(),
(accumulator, child) => {
return accumulator.concat(
child.metadata.requirements.compiled.getAll(),
);
},
contract.metadata.requirements.compiled.getAll(),
);
const conjuncts: Contract[] = contract.metadata.requirements.compiled
.getAll()
.concat(
contract
.getChildren()
.flatMap((child) => child.metadata.requirements.compiled.getAll()),
);

// (1) If the top level list of conjuncts is empty,
// then we can assume the requirements are fulfilled
// and stop without doing any further computations.
if (conjuncts.length === 0) {
return true;
}
// Utilities
const shouldEvaluateType = (type: string) =>
options.types ? options.types.has(type) : true;
/**
* @summary Check if a matcher is satisfied
* @function
* @public
*
* @param {Object} matcher - matcher contract
* @returns {Boolean} whether the matcher is satisfied
*
* @example
* const matcher = Contract.createMatcher({
* type: 'sw.os',
* slug: 'debian'
* })
*
* if (hasMatch(matcher)) {
* console.log('This matcher is satisfied!')
* }
*/
const hasMatch = (matcher: Contract): boolean =>
// TODO: Write a function similar to findContracts
// that stops as soon as it finds one match
this.findChildren(matcher).length > 0;

// (2) The requirements are specified as a list of objects,
// so lets iterate through those.
// This function uses a for loop instead of a more functional
// construct for performance reasons, given that we can freely
// break out of the loop as soon as possible.
for (const conjunct of conjuncts) {
if (conjunct.raw.operation === 'or') {
// (3.1) Note that we should only consider disjuncts
// of types we are allowed to check. We can make
// such transformation here, so we can then consider
// the disjunction as fulfilled if there are no
// remaining disjuncts.
const disjuncts = filter(conjunct.raw.data.getAll(), (disjunct) => {
return shouldEvaluateType(disjunct.raw.data.type);
});
// (3.2) An empty disjuction means that this particular
// requirement is fulfilled, so we can carry on.
// A disjunction naturally contains a list of further
// requirements we need to check for. If at least one
// of the members is fulfilled, we can proceed with
// next requirement.
if (disjuncts.length === 0 || some(map(disjuncts, hasMatch))) {
continue;
}
// (3.3) If no members were fulfilled, then we know
// the whole contract is unsatisfied, so there's no
// reason to keep checking the remaining requirements.
return false;
} else if (conjunct.raw.operation === 'not') {
// (3.4) Note that we should only consider disjuncts
// of types we are allowed to check. We can make
// such transformation here, so we can then consider
// the disjunction as fulfilled if there are no
// remaining disjuncts.
const disjuncts = filter(conjunct.raw.data.getAll(), (disjunct) => {
return shouldEvaluateType(disjunct.raw.data.type);
});
// (3.5) We fail the requirement if the set of negated
// disjuncts is not empty, and we have at least one of
// them in the context.
if (disjuncts.length > 0 && some(map(disjuncts, hasMatch))) {
return false;
}
continue;
}
// (4) If we reached this point, then we know we're dealing
// with a conjunct from the top level *AND* operator.
// Since a logical "and" means that all elements must be
// fulfilled, we can return right away if one of these
// was not satisfied.
if (shouldEvaluateType(conjunct.raw.data.type) && !hasMatch(conjunct)) {
// (3-4) stop looking if an unsatisfied requirement is found
if (!this.isRequirementSatisfied(conjunct, options)) {
return false;
}
}
Expand Down
4 changes: 4 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import Blueprint from './blueprint';
import Universe from './universe';
import { buildTemplate } from './partials';

// this is exported as is one of the return types of
// Contract.getNotSatisfiedChildRequirements
// TODO: remove this comment once the library has correct typings
export { default as ObjectSet } from './object-set';
export {
BlueprintLayout,
ContractObject,
Expand Down
Loading

0 comments on commit 6524517

Please sign in to comment.