-
Notifications
You must be signed in to change notification settings - Fork 102
Node scripts
You can parse .capella files to JSON as .capella files use an XML format.
For instance, using xml-js
const fs = require('fs/promises');
const convert = require('xml-js');
async function parse(filename) {
const data = await fs.readFile(filename, { encoding: 'utf8' });
const options = {compact: true, ignoreComment: true, alwaysArray: true};
return JSON.parse(convert.xml2json(data, options));
}
The raw format of it can be quite complex to be queried/manipulated, as it is from a raw XML. Attributes are stored under _attributes, many attributes and fields would have xmlns: references. We can trim the xmlns using some customisable functions like
const shortenTypes = (val) => val.substring(val.indexOf(":") + 1);
const shortenAttributeType = (val, attr) => attr == "xsi:type" ? val.substring(val.indexOf(":") + 1) : val;
const options = { ... attributesKey: "attrs",
elementNameFn: shortenTypes, attributeNameFn: shortenTypes, attributeValueFn: shortenAttributeType}
Now, an object will look like:
{
"attrs": {
"type": "FunctionalExchange",
"id": "86900f67-a76d-4996-91ce-e94dc33333be",
"name": "Packetized Audio Stream",
"target": "#1dba4c09-f51b-47db-9e69-a2e8e2c8639d",
"source": "#6bd23657-2b1a-42f6-81e3-2e59bcdd15c6"
},
"ownedFunctionalExchangeRealizations": [
{
"attrs": {
"type": "FunctionalExchangeRealization",
"id": "93b6691c-1bc3-4541-8fad-ce62493083a1",
"targetElement": "#223cf5ec-0222-4e32-9646-71bbc85cdd70",
"sourceElement": "#86900f67-a76d-4996-91ce-e94dc33333be"
}
}
]
}
The result json will be a tree of all elements. Look at Metamodel for references between elements.
... "ownedArchitectures": [
{
"attrs": {
"type": "OperationalAnalysis", "id": "e12de7af-bf77-4c43-9f7c-03404be33a29", "name": "Operational Analysis",
},
"ownedFunctionPkg": [
{
"attrs": {
"type": "OperationalActivityPkg", "id": "c2be5628-89b1-482e-bf2c-1199c347b03e", "name": "Operational Activities"
},
"ownedOperationalActivities": [
{
"attrs": {
"type": "OperationalActivity", "id": "d2b0e9ba-7e48-4508-aabc-b4ce66e1cad9", "name": "Root Operational Activity"
},
"ownedOperationalActivities": [
{
"attrs": {
"type": "OperationalActivity", "id": "d2b0e9ba-7e48-4508-aabc-b4ce66e1cad9", "name": "Operational Activity 1"
},
{
"attrs": {
"type": "OperationalActivity", "id": "d2b0e9ba-7e48-4508-aabc-b4ce66e1cad9", "name": "Operational Activity 2"
}
],
"ownedFunctionalChains": [
{
"attrs": {
"type": "OperationalProcess", "id": "e5c28cc3-d846-47fe-98a2-0ccdb2ee512e", "name": "Broadcast Safety Instructions Movie"
},
Some quering tool doesn't support recursive look, like xpath did back then. ~ //[type=SystemFunction]
. Might be good to put elements as flat and add a reference to parent element, might be easier to navigate and use queries afterwards.
Something like:
let flatGraph = (e,p) => Array.isArray(e) ? e.map(c => { c.parent = p; return [c, ...flatGraph(c,p)].flat()}) : typeof(e) !== "object" ? e : [...Object.keys(e).filter(k => k != "attrs" && k != "parent").map(k => { return flatGraph(e[k],e)})].flat();
let result = flatGraph(json,null).flat().filter(e => e.attrs);
{
attrs: {
type: 'OperationalActivity',
id: 'cc39bfa2-731b-448d-a8b7-a45d1cf45a97',
name: 'Broadcast Audio',
availableInStates: '#4a069028-2325-48de-b564-4fef9d6d5a24 #e3993570-4314-45be-a7c0-bc21b1663bc5 #61c11bf7-ea27-40f6-a7a9-361c0482f7f1'
},
parent: {
attrs: [Object],
ownedFunctionalChains: [Array],
ownedFunctions: [Array],
ownedFunctionalExchanges: [Array],
parent: [Object]
}
},
{
attrs: {
type: 'OperationalActivity',
id: '79a50368-c014-4410-aebd-627426b62614',
name: 'Configure Availability Of Services'
},
parent: {
attrs: [Object],
ownedFunctionalChains: [Array],
ownedFunctions: [Array],
ownedFunctionalExchanges: [Array],
parent: [Object]
}
},
{
attrs: {
type: 'FunctionalExchange',
id: '6e25e03b-232a-4e36-89cc-26417caba9e3',
name: 'Aircraft Position',
target: '#f15d0666-1b4a-48d3-a2b1-4206853f1d5a',
source: '#426f80c2-78ad-4e94-8e5b-da19f22aca26'
},
parent: {
attrs: [Object],
ownedFunctionalChains: [Array],
ownedFunctions: [Array],
ownedFunctionalExchanges: [Array],
parent: [Object]
}
},
Can be interesting to retrieve elements by id as well.
let resultById = result.reduce((acc, obj) => acc[obj.attrs.id]=obj, {});
References between elements are referenced by their identifiers. target: '#id1 #id2 #id3' Might be good to reference directly elements rather than their id. Something like:
function toGraph(json) {
let flatGraph = (e,p) => Array.isArray(e) ? e.map(c => { c.parent = p; return [c, ...flatGraph(c,p)].flat()}) : typeof(e) !== "object" ? e : [...Object.keys(e).filter(k => k != "attrs" && k != "parent").map(k => { return flatGraph(e[k],e)})].flat();
let result = flatGraph(json, null).flat().filter(e => e.attrs);
let isReference = (val => typeof(val) == "string" && val.startsWith("#"));
let resultById = result.reduce((acc, obj) => { acc["#"+obj.attrs.id] = obj; return acc; }, {});
let stripUnaryArray = (val) => val.length == 1 ? val[0] : val;
let getValues = (ids) => stripUnaryArray(ids.split(" ").map(id => resultById[id]));
let refs = (r) => r.forEach(element => { Object.keys(element.attrs).filter(a => isReference(element.attrs[a])).forEach(a => element[a] = getValues(element.attrs[a]));});
refs(result);
return result;
}
Now that we have the basic, we can do some shiny stuff.
Using something like jmespath
to navigate through the elements.
async function retrieveAllFunctions(filename) {
const json = await parse(filename);
let result = toGraph(json);
let chains = { chains: result.filter(e => e.attrs.type == "FunctionalChain") };
let involvedFunctions = jmespath.search(chains.chains[0], `ownedFunctionalChainInvolvements[?attrs.type=='FunctionalChainInvolvementFunction'].{ id: attrs.id, name: involved.attrs.name }`)
let involvedExchanges = jmespath.search(chains.chains[0], `ownedFunctionalChainInvolvements[?attrs.type=='FunctionalChainInvolvementLink'].{ id: attrs.id, name: involved.attrs.name, sourceId: source.attrs.id, targetId: target.attrs.id }`)
}
[
{
id: '6ab4adaf-53bf-4c8d-b2d1-88d87529542a',
name: 'Watch Video on Cabin Screen'
},
{
id: '698fef45-16d0-4166-98a9-797776442880',
name: 'Play Airline-Imposed Videos'
}
]
[
{
id: '10eaa967-ff31-4dd1-910a-92a7da7135de',
name: 'Displayed Video',
sourceId: 'a7a779f2-d033-4014-8d19-aecdc216b988',
targetId: '6ab4adaf-53bf-4c8d-b2d1-88d87529542a'
},
{
id: '0f357449-ad6e-482b-982f-1b10c68406f6',
name: 'Audio Stream',
sourceId: 'a7a779f2-d033-4014-8d19-aecdc216b988',
targetId: '322b800c-9c38-4d32-8b9a-bc34fa5c972d'
}
]
Generate some mermaid
diagrams starting from the model
async function generateFC(filename) {
const json = await parse(filename);
let result = toGraph(json);
let chains = { chains: result.filter(e => e.attrs.type == "FunctionalChain") };
let involvedFunctions = jmespath.search(chains.chains[0], `ownedFunctionalChainInvolvements[?attrs.type=='FunctionalChainInvolvementFunction'].{ id: attrs.id, name: involved.attrs.name }`)
let involvedExchanges = jmespath.search(chains.chains[0], `ownedFunctionalChainInvolvements[?attrs.type=='FunctionalChainInvolvementLink'].{ id: attrs.id, name: involved.attrs.name, sourceId: source.attrs.id, targetId: target.attrs.id }`)
flow(involvedFunctions, involvedExchanges);
}
function flow(involvedFunctions, involvedExchanges) {
return `
graph LR
${involvedFunctions.map(e => ""+e.id+"("+e.name+")").join("\n ")}
${involvedExchanges.map(e => ""+e.sourceId+" --> "+e.targetId+"").join("\n ")}
`
}
generateFC("In-Flight Entertainment System.capella");
Output :
graph LR
6ab4adaf-53bf-4c8d-b2d1-88d87529542a(Watch Video on Cabin Screen)
698fef45-16d0-4166-98a9-797776442880(Play Airline-Imposed Videos)
a7a779f2-d033-4014-8d19-aecdc216b988(Broadcast Audio Video Streams)
fbf2bbbd-eb4e-44f5-a972-8bdc89438544(Handle Imposed Videos Controls)
29b534ae-74db-4875-aeaa-5c73fc613bec(Store Digital Media)
59d87294-467d-4a62-9779-469ec28c1d03(Play Airline-Imposed Videos)
322b800c-9c38-4d32-8b9a-bc34fa5c972d(Play Sound in Cabin)
a7a779f2-d033-4014-8d19-aecdc216b988 --> 6ab4adaf-53bf-4c8d-b2d1-88d87529542a
a7a779f2-d033-4014-8d19-aecdc216b988 --> 322b800c-9c38-4d32-8b9a-bc34fa5c972d
322b800c-9c38-4d32-8b9a-bc34fa5c972d --> 6ab4adaf-53bf-4c8d-b2d1-88d87529542a
29b534ae-74db-4875-aeaa-5c73fc613bec --> a7a779f2-d033-4014-8d19-aecdc216b988
59d87294-467d-4a62-9779-469ec28c1d03 --> fbf2bbbd-eb4e-44f5-a972-8bdc89438544
a7a779f2-d033-4014-8d19-aecdc216b988 --> 698fef45-16d0-4166-98a9-797776442880
fbf2bbbd-eb4e-44f5-a972-8bdc89438544 --> a7a779f2-d033-4014-8d19-aecdc216b988
Pie detecting unallocated functions
function pie(a, u) {
return `
pie title Allocated Functions
"Allocated" : ${a}
"Unallocated" : ${u}
`
}
async function retrievePie(filename) {
const json = await parse(filename);
let result = toGraph(json);
let leafFcts = { functions: result.filter(e => e.attrs.type == "LogicalFunction").filter(s => !s.ownedFunctions) };
let cpts = { components: result.filter(e => e.attrs.type == "LogicalComponent") };
let allocatedFunctions = jmespath.search(cpts, `components[*].ownedFunctionalAllocation[*].targetElement.[attrs.id]`).flat(3);
let unallocatedLeafs = leafFcts.functions.filter(x => !allocatedFunctions.includes(x.attrs.id));
console.log(pie(allocatedFunctions.length, unallocatedLeafs.length));
}
pie title Allocated Functions
"Allocated" : 48
"Unallocated" : 4
async function retrieveFCFunctions(filename) {
const json = await parse(filename);
let result = toGraph(json);
let chains = { chains: result.filter(e => e.attrs.type == "FunctionalChain") };
let involvedFunctions = jmespath.search(chains.chains[0], `ownedFunctionalChainInvolvements[?attrs.type=='FunctionalChainInvolvementFunction'].{ id: attrs.id, type: involved.attrs.type, name: involved.attrs.name }`)
let output = involvedFunctions.map(f => `|![](https://raw.githubusercontent.com/eclipse/capella/master/core/plugins/org.polarsys.capella.core.data.res.edit/icons/full/obj16/${f.type}.gif) ${f.name}|${f.id}|\n`).join("");
let result = `
|name|id|
|--|--|
${output}
`
return result;
}
`
name | id |
---|---|
![]() |
6ab4adaf-53bf-4c8d-b2d1-88d87529542a |
![]() |
698fef45-16d0-4166-98a9-797776442880 |
![]() |
a7a779f2-d033-4014-8d19-aecdc216b988 |
![]() |
fbf2bbbd-eb4e-44f5-a972-8bdc89438544 |
![]() |
29b534ae-74db-4875-aeaa-5c73fc613bec |
![]() |
59d87294-467d-4a62-9779-469ec28c1d03 |
![]() |
322b800c-9c38-4d32-8b9a-bc34fa5c972d |
Share your scripts on the Capella forum scripting section.
- Official Website
- Download
- Release-Notes 7.0.0 (current version)
- Release-Notes-6.1.0
- Release-Notes-6.0.0