diff --git a/describe.spec.ts b/describe.spec.ts index 88d46008..6d04ffa7 100644 --- a/describe.spec.ts +++ b/describe.spec.ts @@ -149,15 +149,9 @@ const baseEnumType = testScl.querySelector("#someID")!; const diffEnumType = testScl.querySelector("#someDiffID")!; const equalEnumType = testScl.querySelector("#someOtherID")!; -const baseServer = testScl.querySelector( - `IED[name="IED1"] LDevice[inst="ldInst1"]`, -)!; -const equalServer = testScl.querySelector( - `IED[name="IED2"] LDevice[inst="ldInst2"]`, -)!; -const diffServer = testScl.querySelector( - `IED[name="IED2"] LDevice[inst="ldInst3"]`, -)!; +const baseAP = testScl.querySelector(`IED[name="IED1"]>AccessPoint`)!; +const equalAP = testScl.querySelector(`IED[name="IED2"]>AccessPoint`)!; +const diffAP = testScl.querySelector(`IED[name="IED3"]>AccessPoint`)!; describe("Describe SCL elements function", () => { it("returns undefined with missing describe function", () => @@ -176,13 +170,13 @@ describe("Describe SCL elements function", () => { JSON.stringify(describeSclElement(equalEnumType)), )); - it("returns same description with semantically equal LDevice's", () => - expect(JSON.stringify(describeSclElement(baseServer))).to.equal( - JSON.stringify(describeSclElement(equalServer)), + it("returns same description with semantically equal AccessPoint's", () => + expect(JSON.stringify(describeSclElement(baseAP))).to.equal( + JSON.stringify(describeSclElement(equalAP)), )); - it("returns different description with unequal LDevice elements", () => - expect(JSON.stringify(describeSclElement(baseServer))).to.not.equal( - JSON.stringify(describeSclElement(diffServer)), + it("returns different description with unequal AccessPoint elements", () => + expect(JSON.stringify(describeSclElement(baseAP))).to.not.equal( + JSON.stringify(describeSclElement(diffAP)), )); }); diff --git a/describe.ts b/describe.ts index 44227632..de21ffac 100644 --- a/describe.ts +++ b/describe.ts @@ -1,3 +1,4 @@ +import { AccessPoint, AccessPointDescription } from "./describe/AccessPoint.js"; import { Private, PrivateDescription } from "./describe/Private.js"; import { Text, TextDescription } from "./describe/Text.js"; import { EnumType, EnumTypeDescription } from "./describe/EnumType.js"; @@ -22,7 +23,8 @@ export type Description = | LN0Description | LDeviceDescription | ServerDescription - | ServicesDescription; + | ServicesDescription + | AccessPointDescription; const sclElementDescriptors: Partial< Record Description | undefined> > = { @@ -37,6 +39,7 @@ const sclElementDescriptors: Partial< LDevice, Server, Services, + AccessPoint, }; export function describe(element: Element): Description | undefined { diff --git a/describe/AccessPoint.spec.ts b/describe/AccessPoint.spec.ts new file mode 100644 index 00000000..3390f454 --- /dev/null +++ b/describe/AccessPoint.spec.ts @@ -0,0 +1,221 @@ +import { expect } from "chai"; +import { AccessPoint } from "./AccessPoint"; + +const scl = new DOMParser().parseFromString( + ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + off + + + + + + + + + + + + + + 60.60 + 10.10 + 40.10 + + + + on + test + off + + + `, + "application/xml", +); + +const baseAP = scl.querySelector('IED[name="IED1"]>AccessPoint')!; +const equalAP = scl.querySelector('IED[name="IED2"]>AccessPoint')!; +const diffAP = scl.querySelector('IED[name="IED3"]>AccessPoint')!; +const invalidAP1 = scl.querySelector('IED[name="IED4"]>AccessPoint')!; +const invalidAP2 = scl.querySelector('IED[name="IED5"]>AccessPoint')!; +const invalidAP3 = scl.querySelector('IED[name="IED6"]>AccessPoint')!; + +describe("Description for SCL schema type AccessPoint", () => { + it("returns undefined with invalid Server", () => + expect(AccessPoint(invalidAP1)).to.be.undefined); + + it("return undefined with invalid LN", () => + expect(AccessPoint(invalidAP2)).to.be.undefined); + + it("return undefined with invalid ServerAt", () => + expect(AccessPoint(invalidAP3)).to.be.undefined); + + it("return router attribute defaulting to false", () => + expect(AccessPoint(baseAP)?.router).to.be.false); + + it("return clock attribute defaulting to false", () => + expect(AccessPoint(baseAP)?.clock).to.be.false); + + it("return kdc attribute defaulting to false", () => + expect(AccessPoint(baseAP)?.kdc).to.be.false); + + it("returns same description with semantically equal LDevice's", () => + expect(JSON.stringify(AccessPoint(baseAP))).to.equal( + JSON.stringify(AccessPoint(equalAP)), + )); + + it("returns different description with unequal LDevice elements", () => + expect(JSON.stringify(AccessPoint(baseAP))).to.not.equal( + JSON.stringify(AccessPoint(diffAP)), + )); +}); diff --git a/describe/AccessPoint.ts b/describe/AccessPoint.ts new file mode 100644 index 00000000..b9f230d4 --- /dev/null +++ b/describe/AccessPoint.ts @@ -0,0 +1,143 @@ +import { sortRecord } from "../utils.js"; +import { LNDescription, sortedLNDescriptions } from "./LN.js"; +import { describeNaming, NamingDescription } from "./Naming.js"; +import { Server, ServerDescription } from "./Server.js"; +import { Services, ServicesDescription } from "./Services.js"; + +interface Cert { + /** Cert attribute commonName */ + commonName: string; + /** Cert attribute idHierarchy */ + idHierarchy: string; +} + +export interface Certificate extends NamingDescription { + /** Certificate attribute serialNumber */ + serialNumber: number; + /** Certificate attribute xferNumber */ + xferNumber?: number; + /** Certificate child Subject */ + subject: Cert; + /** Certificate child IssuerName */ + issuerName: Cert; +} + +interface ServerAtDescription extends NamingDescription { + /** ServerAt attribute apName */ + apName: string; +} + +export interface AccessPointDescription extends NamingDescription { + /** AccessPoint attribute router defaulted false */ + router: boolean; + /** AccessPoint attribute clock defaulted false */ + clock: boolean; + /** AccessPoint attribute kdc defaulted false */ + kdc: boolean; + /** AccessPoint child Services */ + services?: ServicesDescription; + /** AccessPoint child Server */ + server?: ServerDescription; + /** AccessPoint children ServerAt */ + serverAt?: ServerAtDescription; + /** AccessPoint children LN */ + lns: Record; + /** AccessPoint children GOOSESecurity */ + gooseSecurities: Record; + /** AccessPoint children SMVSecurity */ + smvSecurities: Record; +} + +function describeServerAt(element: Element): ServerAtDescription | undefined { + const apName = element.getAttribute("apName"); + if (!apName) return; + + return { ...describeNaming(element), apName }; +} + +function describeCert(element: Element): Cert | undefined { + const commonName = element.getAttribute("commonName"); + if (!commonName) return; + + const idHierarchy = element.getAttribute("idHierarchy"); + if (!idHierarchy) return; + + return { commonName, idHierarchy }; +} + +function describeCertificate(element: Element): Certificate | undefined { + const serialNumber = element.getAttribute("serialNumber"); + if (!serialNumber) return; + + const subjectElement = element.querySelector(":scope > Subject"); + if (!subjectElement) return; + const subject = describeCert(subjectElement); + if (!subject) return; + + const issuerNameElement = element.querySelector(":scope > IssuerName"); + if (!issuerNameElement) return; + const issuerName = describeCert(issuerNameElement); + if (!issuerName) return; + + const certificate: Certificate = { + ...describeNaming(element), + serialNumber: parseInt(serialNumber, 10), + subject, + issuerName, + }; + + return certificate; +} + +function certificates( + element: Element, + type: "GOOSESecurity" | "SMVSecurity", +): Record { + const certificates: Record = {}; + + element.querySelectorAll(`:scope > ${type}`).forEach((certificate) => { + const name = certificate.getAttribute("name"); + const certificateDescription = describeCertificate(certificate); + if (name && certificateDescription) + certificates[name] = certificateDescription; + }); + + return sortRecord(certificates) as Record; +} + +export function AccessPoint( + element: Element, +): AccessPointDescription | undefined { + const lns = sortedLNDescriptions(element); + if (!lns) return; + + const accessPointDescription: AccessPointDescription = { + ...describeNaming(element), + router: element.getAttribute("router") === "true" ? true : false, + clock: element.getAttribute("clock") === "true" ? true : false, + kdc: element.getAttribute("kdc") === "true" ? true : false, + lns, + gooseSecurities: certificates(element, "GOOSESecurity"), + smvSecurities: certificates(element, "SMVSecurity"), + }; + + const servicesElement = element.querySelector(":scope > Services"); + if (servicesElement) + accessPointDescription.services = Services(servicesElement); + + const serverElement = element.querySelector(":scope > Server"); + if (serverElement) { + const server = Server(serverElement); + if (server) accessPointDescription.server = server; + else return; + } + + const serverAtElement = element.querySelector(":scope > ServerAt"); + if (serverAtElement) { + const serverAt = describeServerAt(serverAtElement); + if (serverAt) accessPointDescription.serverAt = serverAt; + else return; + } + + return accessPointDescription; +} diff --git a/describe/LDevice.ts b/describe/LDevice.ts index 48b99b0b..2ad7f2df 100644 --- a/describe/LDevice.ts +++ b/describe/LDevice.ts @@ -1,7 +1,6 @@ import { LN0, LN0Description } from "./LN0.js"; -import { LN, LNDescription } from "./LN.js"; +import { LNDescription, sortedLNDescriptions } from "./LN.js"; import { NamingDescription, describeNaming } from "./Naming.js"; -import { sortRecord } from "../utils.js"; export interface LDeviceDescription extends NamingDescription { /** LDevice attribute ldName */ @@ -21,36 +20,13 @@ export function LDevice(element: Element): LDeviceDescription | undefined { const ln0Description = LN0(ln0); if (!ln0Description) return; - const lns: Record = {}; - let existUndefinedLns = false; - Array.from(element.children) - .filter((child) => child.tagName === "LN") - .forEach((ln) => { - const prefix = ln.getAttribute("prefix"); - const lnClass = ln.getAttribute("lnClass"); - const inst = ln.getAttribute("inst"); - if (!lnClass || !inst) { - existUndefinedLns = true; - return; - } - - const id = `${prefix ? prefix : ""}${lnClass}${inst}`; - - const lnDescription = LN(ln); - if (!lnDescription) { - existUndefinedLns = true; - return; - } - - lns[id] = lnDescription; - }); - - if (existUndefinedLns) return; + const lns = sortedLNDescriptions(element); + if (!lns) return; const lDeviceDescription: LDeviceDescription = { ...describeNaming(element), ln0: ln0Description, - lns: sortRecord(lns) as Record, + lns, }; const ldName = element.getAttribute("ldName"); diff --git a/describe/LN.ts b/describe/LN.ts index 7a942fe5..54af6de7 100644 --- a/describe/LN.ts +++ b/describe/LN.ts @@ -17,6 +17,37 @@ import { } from "./ReportControl.js"; import { describeVal, compareBySGroup } from "./Val.js"; +export function sortedLNDescriptions( + parent: Element, +): Record | undefined { + const lns: Record = {}; + let existUndefinedLns = false; + Array.from(parent.children) + .filter((child) => child.tagName === "LN") + .forEach((ln) => { + const prefix = ln.getAttribute("prefix"); + const lnClass = ln.getAttribute("lnClass"); + const inst = ln.getAttribute("inst"); + if (!lnClass || !inst) { + existUndefinedLns = true; + return; + } + + const id = `${prefix ? prefix : ""}${lnClass}${inst}`; + + const lnDescription = LN(ln); + if (!lnDescription) { + existUndefinedLns = true; + return; + } + + lns[id] = lnDescription; + }); + if (existUndefinedLns) return; + + return sortRecord(lns) as Record; +} + export interface LNDescription extends NamingDescription { reports: Record; logControls: Record; diff --git a/utils.ts b/utils.ts index 15193671..25dae21d 100644 --- a/utils.ts +++ b/utils.ts @@ -1,3 +1,4 @@ +import { Certificate } from "./describe/AccessPoint.js"; import { DADescription } from "./describe/DADescription.js"; import { DODescription } from "./describe/DODescription.js"; import { GSEControlDescription } from "./describe/GSEControl.js"; @@ -10,6 +11,7 @@ import { SDODescription } from "./describe/SDODescription.js"; import { SampledValueControlDescription } from "./describe/SampledValueControl.js"; type SortedObjects = + | Certificate | DADescription | GSEControlDescription | LDeviceDescription