-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement reference utilities (like the ones in the C# impl.)
- Loading branch information
Showing
7 changed files
with
317 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import {chain, concatenator, LanguageFactory, lastOf} from "@lionweb/core" | ||
import {hasher} from "@lionweb/utilities" | ||
|
||
|
||
const factory = new LanguageFactory( | ||
"Generic", | ||
"0", | ||
chain(concatenator("-"), hasher({ encoding: "base64" })), | ||
lastOf | ||
) | ||
export const genericLanguage = factory.language | ||
|
||
export const SomeConcept = factory.concept("SomeConcept", false) | ||
export const AnotherConcept = factory.concept("AnotherConcept", false) | ||
|
||
export const SomeConcept_ref = factory.reference(SomeConcept, "ref").ofType(AnotherConcept).isOptional() | ||
export const SomeConcept_refs = factory.reference(SomeConcept, "refs").ofType(AnotherConcept).isMultiple() | ||
SomeConcept.havingFeatures(SomeConcept_ref, SomeConcept_refs) | ||
|
||
export const SomeAnnotation = factory.annotation("SomeAnnotation") | ||
SomeAnnotation.annotates = AnotherConcept | ||
export const SomeAnnotation_ref = factory.reference(SomeAnnotation, "ref").ofType(SomeConcept) | ||
SomeAnnotation.havingFeatures(SomeAnnotation_ref) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import {chain, concatenator, LanguageFactory, lastOf} from "@lionweb/core" | ||
import {hasher} from "@lionweb/utilities" | ||
|
||
|
||
const factory = new LanguageFactory( | ||
"TinyRef", | ||
"0", | ||
chain(concatenator("-"), hasher({ encoding: "base64" })), | ||
lastOf | ||
) | ||
export const tinyRefLanguage = factory.language | ||
|
||
export const MyConcept = factory.concept("MyConcept", false) | ||
export const MyConcept_singularRef = factory.reference(MyConcept, "singularRef").ofType(MyConcept) | ||
export const MyConcept_multivaluedRef = factory.reference(MyConcept, "multivaluedRef").ofType(MyConcept).isMultiple() | ||
MyConcept.havingFeatures(MyConcept_singularRef, MyConcept_multivaluedRef) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import {assert} from "chai" | ||
const {deepEqual} = assert | ||
|
||
import {Classifier, dynamicExtractionFacade, dynamicInstantiationFacade, DynamicNode} from "@lionweb/core" | ||
import {incomingReferences, referencesToOutOfScopeNodes, ReferenceValue, referenceValues} from "@lionweb/utilities" | ||
|
||
import {AnotherConcept, SomeAnnotation, SomeAnnotation_ref, SomeConcept, SomeConcept_ref} from "../languages/generic.js" | ||
import {MyConcept, MyConcept_multivaluedRef, MyConcept_singularRef} from "../languages/tiny-ref.js" | ||
|
||
|
||
/* | ||
* These unit tests are pretty much a straight-up copy of the ones in the ReferenceUtilsTests class in the LionWeb C# implementation: | ||
* https://github.com/LionWeb-io/lionweb-csharp/blob/main/test/LionWeb-CSharp-Test/tests/ReferenceUtilsTests.cs . | ||
*/ | ||
|
||
describe("reference utils", () => { | ||
|
||
const createNode = (id: string, classifier: Classifier): DynamicNode => ({ | ||
id, | ||
classifier, | ||
settings: {}, | ||
annotations: [] | ||
}) | ||
const setValue = dynamicInstantiationFacade.setFeatureValue | ||
|
||
it("find a reference from a feature of a concept", () => { | ||
const targetNode = createNode("target", AnotherConcept) | ||
const sourceNode = createNode("source", SomeConcept) | ||
setValue(sourceNode, SomeConcept_ref, targetNode) | ||
const scope = [sourceNode, targetNode] | ||
|
||
const expectedRefs = [ | ||
new ReferenceValue(sourceNode, targetNode, SomeConcept_ref, null) | ||
] | ||
deepEqual(referenceValues(scope, dynamicExtractionFacade), expectedRefs) | ||
deepEqual(incomingReferences(targetNode, scope, dynamicExtractionFacade), expectedRefs) | ||
}) | ||
|
||
it("find a reference from an annotation", () => { | ||
const targetNode = createNode("target", SomeConcept) | ||
const sourceNode = createNode("source", SomeAnnotation) | ||
setValue(sourceNode, SomeAnnotation_ref, targetNode) | ||
const sourceContainer = createNode("sourceContainer", AnotherConcept) | ||
sourceNode.annotations.push(sourceNode) | ||
const scope = [sourceContainer, sourceNode, targetNode] | ||
|
||
/* | ||
* Note: this doesn't actually test anything new relative to the 1st test, | ||
* because the annotation [instance] has a reference just like a concept instance has, | ||
* and because the annotation is explicitly declared as part of the scope. | ||
* It's more interesting to verify that a scope computed from root nodes would contain the annotation! | ||
*/ | ||
deepEqual( | ||
incomingReferences(targetNode, scope, dynamicExtractionFacade), | ||
[ | ||
new ReferenceValue(sourceNode, targetNode, SomeAnnotation_ref, null) | ||
] | ||
) | ||
}) | ||
|
||
it("find a reference to itself", () => { | ||
const node = createNode("node", MyConcept) | ||
setValue(node, MyConcept_singularRef, node) | ||
|
||
deepEqual( | ||
incomingReferences(node, [node], dynamicExtractionFacade), | ||
[ | ||
new ReferenceValue(node, node, MyConcept_singularRef, null) | ||
] | ||
) | ||
}) | ||
|
||
it("find references in different features of the source", () => { | ||
const targetNode = createNode("target", MyConcept) | ||
const sourceNode = createNode("source", MyConcept) | ||
setValue(sourceNode, MyConcept_singularRef, targetNode) | ||
setValue(sourceNode, MyConcept_multivaluedRef, targetNode) | ||
deepEqual(sourceNode.settings["multivaluedRef"], [targetNode]) // assert that setValue(<node>, <multivalued feature>, <value>) _added_ the value | ||
|
||
deepEqual( | ||
incomingReferences(targetNode, [sourceNode], dynamicExtractionFacade), | ||
[ | ||
new ReferenceValue(sourceNode, targetNode, MyConcept_singularRef, null), | ||
new ReferenceValue(sourceNode, targetNode, MyConcept_multivaluedRef, 0) | ||
] | ||
) | ||
}) | ||
|
||
it("find multiple references to target in a multivalued feature of the source", () => { | ||
const targetNode = createNode("target", MyConcept) | ||
const sourceNode = createNode("source", MyConcept) | ||
setValue(sourceNode, MyConcept_multivaluedRef, targetNode) | ||
setValue(sourceNode, MyConcept_multivaluedRef, targetNode) | ||
deepEqual(sourceNode.settings["multivaluedRef"], [targetNode, targetNode]) // assert that setValue(<node>, <multivalued feature>, <value>) _added_ the values | ||
|
||
deepEqual( | ||
incomingReferences(targetNode, [sourceNode], dynamicExtractionFacade), | ||
[ | ||
new ReferenceValue(sourceNode, targetNode, MyConcept_multivaluedRef, 0), | ||
new ReferenceValue(sourceNode, targetNode, MyConcept_multivaluedRef, 1) | ||
] | ||
) | ||
}) | ||
|
||
it("find references among multiple sources and targets", () => { | ||
const sourceNode1 = createNode("sourceNode1", MyConcept) | ||
const sourceNode2 = createNode("sourceNode2", MyConcept) | ||
const targetNode1 = createNode("targetNode1", MyConcept) | ||
const targetNode2 = createNode("targetNode2", MyConcept) | ||
setValue(sourceNode1, MyConcept_singularRef, targetNode1) | ||
setValue(sourceNode2, MyConcept_singularRef, targetNode2) | ||
|
||
deepEqual( | ||
incomingReferences([targetNode1, targetNode2], [sourceNode1, sourceNode2], dynamicExtractionFacade), | ||
[ | ||
new ReferenceValue(sourceNode1, targetNode1, MyConcept_singularRef, null), | ||
new ReferenceValue(sourceNode2, targetNode2, MyConcept_singularRef, null) | ||
] | ||
) | ||
}) | ||
|
||
it("have defined behavior for duplicate target nodes", () => { | ||
const targetNode = createNode("target", AnotherConcept) | ||
const sourceNode = createNode("source", SomeConcept) | ||
setValue(sourceNode, SomeConcept_ref, targetNode) | ||
const scope = [sourceNode, targetNode] | ||
|
||
const expectedRefs = [ | ||
new ReferenceValue(sourceNode, targetNode, SomeConcept_ref, null) | ||
] | ||
const duplicateTargetNodes = [targetNode, targetNode] | ||
deepEqual(incomingReferences(duplicateTargetNodes, scope, dynamicExtractionFacade), expectedRefs) | ||
deepEqual(referenceValues(scope, dynamicExtractionFacade), expectedRefs) | ||
}) | ||
|
||
it("have defined behavior when duplicate nodes in scope", () => { | ||
const targetNode = createNode("target", AnotherConcept) | ||
const sourceNode = createNode("source", SomeConcept) | ||
setValue(sourceNode, SomeConcept_ref, targetNode) | ||
const scope = [sourceNode, targetNode] | ||
|
||
const expectedRefs = [ | ||
new ReferenceValue(sourceNode, targetNode, SomeConcept_ref, null) | ||
] | ||
const duplicateScope = [...scope, ...scope] | ||
deepEqual(incomingReferences(targetNode, duplicateScope, dynamicExtractionFacade), expectedRefs) | ||
deepEqual(referenceValues(duplicateScope, dynamicExtractionFacade), expectedRefs) | ||
}) | ||
|
||
it("find unreachable nodes", () => { | ||
const targetNode = createNode("target", AnotherConcept) | ||
const sourceNode = createNode("source", SomeConcept) | ||
setValue(sourceNode, SomeConcept_ref, targetNode) | ||
|
||
deepEqual( | ||
referencesToOutOfScopeNodes([sourceNode, sourceNode], dynamicExtractionFacade), // Note: scope is duplicate | ||
[ | ||
new ReferenceValue(sourceNode, targetNode, SomeConcept_ref, null) | ||
] | ||
) | ||
}) | ||
|
||
}) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
export * from "./hashing.js" | ||
export * from "./utils/json.js" | ||
export * from "./m1/reference-utils.js" | ||
export * from "./m3/index.js" | ||
export * from "./serialization/index.js" | ||
export * from "./m3/infer-languages.js" | ||
export * from "./utils/json.js" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import {allFeaturesOf, ExtractionFacade, Node, Reference} from "@lionweb/core" | ||
|
||
|
||
/** | ||
* Represents information about a source and target node related through a {@link Reference}. | ||
* An index of `null` means that the reference is (at most) single-valued. | ||
*/ | ||
export class ReferenceValue<NT extends Node> { | ||
constructor( | ||
public readonly sourceNode: NT, | ||
public readonly targetNode: NT, | ||
public readonly reference: Reference, | ||
public readonly index: number | null | ||
) {} | ||
} | ||
|
||
|
||
/** | ||
* Finds all references within the given scope, as {@link ReferenceValue reference values}. | ||
* To search within all nodes under a collection of root nodes, | ||
* use _child extraction_ to compute all nodes in the forest hanging off of those root nodes as scope. | ||
* Note that any reference is found uniquely, | ||
* i.e. the returned {@link ReferenceValue reference values} are pairwise distinct, | ||
* even if the scope passed contains duplicate nodes. | ||
* | ||
* @param scope - the {@link Node nodes} that are searched for references | ||
* @param extractionFacade - an {@link ExtractionFacade} to reflect on nodes. | ||
* _Note_ that it's assumed that its {@link getFeatureValue} function doesn't throw. | ||
*/ | ||
export const referenceValues = <NT extends Node>( | ||
scope: NT[], | ||
extractionFacade: ExtractionFacade<NT> | ||
): ReferenceValue<NT>[] => { | ||
const visit = (sourceNode: NT, reference: Reference): ReferenceValue<NT>[] => { | ||
if (reference.multiple) { | ||
const targetNodes = extractionFacade.getFeatureValue(sourceNode, reference) as NT[] ?? [] | ||
return targetNodes | ||
.map((targetNode, index) => | ||
new ReferenceValue<NT>(sourceNode, targetNode, reference, index) | ||
) | ||
} | ||
|
||
const targetNode = extractionFacade.getFeatureValue(sourceNode, reference) as (NT | undefined) | ||
if (targetNode !== undefined) { | ||
return [new ReferenceValue<NT>(sourceNode, targetNode, reference, null)] | ||
} | ||
|
||
return [] | ||
} | ||
|
||
return [...new Set(scope)] // ~ .distinct() | ||
.flatMap((sourceNode) => | ||
allFeaturesOf(extractionFacade.classifierOf(sourceNode)) | ||
.filter((feature) => feature instanceof Reference) | ||
.map((feature) => feature as Reference) | ||
.flatMap((reference) => visit(sourceNode, reference)) | ||
) | ||
} | ||
|
||
|
||
/** | ||
* Finds all references coming into the given target node or any of the given target nodes, | ||
* within the given scope, as {@link ReferenceValue reference values}. | ||
* To search within all nodes under a collection of root nodes, | ||
* use _child extraction_ to compute all nodes in the forest hanging off of those root nodes as scope. | ||
* Note that any reference is found uniquely, | ||
* i.e. the returned {@link ReferenceValue reference values} are pairwise distinct, | ||
* even if the given target nodes or scope contain duplicate nodes. | ||
* | ||
* @param targetNodeOrNodes - one or more target {@link Node nodes} for which the incoming references are searched | ||
* @param scope - the {@link Node nodes} that are searched for references | ||
* @param extractionFacade - an {@link ExtractionFacade} to reflect on nodes. | ||
* _Note_ that it's assumed that its {@link getFeatureValue} function doesn't throw. | ||
*/ | ||
export const incomingReferences = <NT extends Node>( | ||
targetNodeOrNodes: NT[] | NT, | ||
scope: NT[], | ||
extractionFacade: ExtractionFacade<NT> | ||
): ReferenceValue<NT>[] => { | ||
const targetNodes = Array.isArray(targetNodeOrNodes) ? targetNodeOrNodes : [targetNodeOrNodes] | ||
return referenceValues(scope, extractionFacade) | ||
.filter((referenceValue) => targetNodes.indexOf(referenceValue.targetNode) > -1) | ||
} | ||
|
||
|
||
/** | ||
* Finds all references to nodes that are not in the given scope, as {@link ReferenceValue reference values}. | ||
* To search within all nodes under a collection of root nodes, | ||
* use _child extraction_ to compute all nodes in the forest hanging off of those root nodes as scope. | ||
* Note that any reference is found uniquely, | ||
* i.e. the returned {@link ReferenceValue reference values} are pairwise distinct, | ||
* even if the given scope contains duplicate nodes. | ||
* | ||
* @param scope - the {@link Node nodes} that form the scope of “reachable” nodes | ||
* @param extractionFacade - an {@link ExtractionFacade} to reflect on nodes. | ||
* _Note_ that it's assumed that its {@link getFeatureValue} function doesn't throw. | ||
*/ | ||
export const referencesToOutOfScopeNodes = <NT extends Node>( | ||
scope: NT[], | ||
extractionFacade: ExtractionFacade<NT> | ||
): ReferenceValue<NT>[] => | ||
referenceValues(scope, extractionFacade) | ||
.filter((referenceValue) => scope.indexOf(referenceValue.targetNode) === -1) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
export * from "./diagrams/Mermaid-generator.js" | ||
export * from "./diagrams/PlantUML-generator.js" | ||
export * from "./ts-generation/ts-types-generator.js" | ||
export * from "./infer-languages.js" | ||
export * from "./textualizer.js" | ||
export * from "./ts-generation/ts-types-generator.js" |