Skip to content

Node scripts

Philippe DUL edited this page May 5, 2023 · 4 revisions

How to parse .capella files

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;
}

Do some nice stuff

Now that we have the basic, we can do some shiny stuff.

Quering

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 mermaid diagrams

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
Loading

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
Loading

Generate some tables

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
Watch Video on Cabin Screen 6ab4adaf-53bf-4c8d-b2d1-88d87529542a
Play Airline-Imposed Videos 698fef45-16d0-4166-98a9-797776442880
Broadcast Audio Video Streams a7a779f2-d033-4014-8d19-aecdc216b988
Handle Imposed Videos Controls fbf2bbbd-eb4e-44f5-a972-8bdc89438544
Store Digital Media 29b534ae-74db-4875-aeaa-5c73fc613bec
Play Airline-Imposed Videos 59d87294-467d-4a62-9779-469ec28c1d03
Play Sound in Cabin 322b800c-9c38-4d32-8b9a-bc34fa5c972d

Share

Share your scripts on the Capella forum scripting section.

Clone this wiki locally