Skip to content

Commit 6fbf9d2

Browse files
committed
feat: start work on security
1 parent 8649802 commit 6fbf9d2

File tree

3 files changed

+236
-17
lines changed

3 files changed

+236
-17
lines changed

node/td-utils/src/expand.ts

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,52 @@ const topLevelConnectionDefinitions = "connectionDefinitions";
2929
const topLevelSecurityKey = "security";
3030
const topLevelSecurityDefinitions = "securityDefinitions";
3131

32-
export function expandTD(inputTD: ThingDescription): ThingDescription | undefined {
33-
// in case of single form or connectin
32+
// Expand a TD with form/connection/security definitions at the top level into each interaction affordance
33+
export function expandTD(inputTD: any): ThingDescription | undefined {
34+
// in case of single form or connection
3435
let defaultForm: any = {};
3536
let defaultConnection: any = {};
37+
let defaultSecurity: any = {};
3638

3739
// in case of multiple form or connections
3840
let defaultFormArray: any = [];
3941
let defaultConnectionArray: any = [];
42+
let defaultSecurityArray: any = [];
43+
44+
// finding default security(s) based on the top level "security" and "securityDefinitions" keys
45+
if (topLevelSecurityKey in inputTD) {
46+
const topLevelSecurity = (inputTD as any)[topLevelSecurityKey];
47+
48+
if (Array.isArray(topLevelSecurity)) {
49+
if (topLevelSecurity.length > 1) {
50+
defaultSecurityArray = topLevelSecurity.map((secKey: string) => {
51+
return (inputTD as any)[topLevelSecurityDefinitions]?.[secKey];
52+
});
53+
54+
delete inputTD[topLevelSecurityKey];
55+
} else if (topLevelSecurity.length === 1) {
56+
defaultSecurity = {security:(inputTD as any)[topLevelSecurityDefinitions]?.[topLevelSecurity[0]]};
57+
delete inputTD[topLevelSecurityKey];
58+
} else if (topLevelSecurity.length === 0) {
59+
throw new Error("Empty security array is not allowed");
60+
} else {
61+
// should not be possible. throw error
62+
throw new Error("Badly formatted security array");
63+
}
64+
} else if (typeof topLevelSecurity === "object" && topLevelSecurity !== null) {
65+
// Check if object is empty
66+
if (Object.keys(topLevelSecurity).length === 0) {
67+
throw new Error("Empty security object is not allowed");
68+
}
69+
defaultSecurity = {security:topLevelSecurity};
70+
delete inputTD[topLevelSecurityKey];
71+
} else {
72+
// only object or array is allowed. return error
73+
throw new Error("Only object or array is allowed for the security key in the top level");
74+
}
75+
} else {
76+
// no top level security to expand. There can be in a form or connection in the top level.
77+
}
4078

4179
// finding default connection(s) based on the top level "connection" and "connectionDefinitions" keys
4280
if (topLevelConnectionKey in inputTD) {
@@ -48,10 +86,10 @@ export function expandTD(inputTD: ThingDescription): ThingDescription | undefine
4886
return (inputTD as any)[topLevelConnectionDefinitions]?.[connKey];
4987
});
5088

51-
delete inputTD.connection;
89+
delete inputTD[topLevelConnectionKey];
5290
} else if (topLevelConnection.length === 1) {
5391
defaultConnection = (inputTD as any)[topLevelConnectionDefinitions]?.[topLevelConnection[0]];
54-
delete inputTD.connection;
92+
delete inputTD[topLevelConnectionKey];
5593
} else if (topLevelConnection.length === 0) {
5694
throw new Error("Empty connection array is not allowed");
5795
} else {
@@ -64,7 +102,7 @@ export function expandTD(inputTD: ThingDescription): ThingDescription | undefine
64102
throw new Error("Empty connection object is not allowed");
65103
}
66104
defaultConnection = topLevelConnection;
67-
delete inputTD.connection;
105+
delete inputTD[topLevelConnectionKey];
68106
} else {
69107
// only object or array is allowed. return error
70108
throw new Error("Only object or array is allowed for the connection key in the top level");
@@ -82,10 +120,10 @@ export function expandTD(inputTD: ThingDescription): ThingDescription | undefine
82120
return (inputTD as any)[topLevelFormDefinitions]?.[formKey];
83121
});
84122

85-
delete inputTD.form;
123+
delete inputTD[topLevelFormKey];
86124
} else if (topLevelForm.length === 1) {
87125
defaultForm = (inputTD as any)[topLevelFormDefinitions]?.[topLevelForm[0]];
88-
delete inputTD.form;
126+
delete inputTD[topLevelFormKey];
89127
} else if (topLevelForm.length === 0) {
90128
throw new Error("Empty form array is not allowed");
91129
} else {
@@ -98,7 +136,7 @@ export function expandTD(inputTD: ThingDescription): ThingDescription | undefine
98136
throw new Error("Empty form object is not allowed");
99137
}
100138
defaultForm = topLevelForm;
101-
delete inputTD.form;
139+
delete inputTD[topLevelFormKey];
102140
} else {
103141
// only object or array is allowed. return error
104142
throw new Error("Only non-empty object or array is allowed for the form key in the top level");
@@ -107,13 +145,29 @@ export function expandTD(inputTD: ThingDescription): ThingDescription | undefine
107145
// no top level form to expand. There can be connection etc. in the top level.
108146
}
109147

110-
// if defaultForm and defaultConnection are filled with items, merge them. defaultForm takes precedence
111-
if (Object.keys(defaultForm).length > 0 && Object.keys(defaultConnection).length > 0) {
112-
defaultForm = { ...defaultConnection, ...defaultForm };
113-
} else if (Object.keys(defaultConnection).length > 0) {
114-
defaultForm = defaultConnection;
148+
console.log("Default form before merging:", defaultForm);
149+
console.log("Default connection before merging:", defaultConnection);
150+
console.log("Default security before merging:", defaultSecurity);
151+
152+
// if defaultForm, defaultConnection and defaultSecurity are filled with items, merge them. defaultForm > defaultConnection > defaultSecurity
153+
// The merging happens in groups of two but only if both are objects and not empty due to initialization as {}
154+
if (typeof defaultConnection === "object" && Object.keys(defaultConnection).length > 0 && typeof defaultSecurity === "object" && Object.keys(defaultSecurity).length > 0) {
155+
if (Object.keys(defaultConnection).length > 0 && Object.keys(defaultSecurity).length > 0) {
156+
defaultConnection = { defaultSecurity, ...defaultConnection };
157+
} else if (Object.keys(defaultSecurity).length > 0) {
158+
defaultConnection = defaultSecurity;
159+
}
160+
}
161+
162+
console.log("Default connection after merging security:", defaultConnection);
163+
if (typeof defaultConnection === "object" && Object.keys(defaultConnection).length > 0 && typeof defaultForm === "object" && Object.keys(defaultForm).length > 0) {
164+
if (Object.keys(defaultForm).length > 0 && Object.keys(defaultConnection).length > 0) {
165+
defaultForm = { ...defaultConnection, ...defaultForm };
166+
} else if (Object.keys(defaultConnection).length > 0) {
167+
defaultForm = defaultConnection;
168+
}
169+
defaultFormArray[0] = defaultForm;
115170
}
116-
defaultFormArray[0] = defaultForm;
117171

118172
// // like above but for the array case. However, each array needs to be merged like a matrix multiplication. form array of length 2 and connection array of length 3 results in 6 forms
119173
if (defaultFormArray.length > 0 && defaultConnectionArray.length > 0) {
@@ -124,6 +178,7 @@ export function expandTD(inputTD: ThingDescription): ThingDescription | undefine
124178
}
125179
}
126180
defaultFormArray = mergedFormArray;
181+
console.log("Merged default form array:", defaultFormArray);
127182
delete inputTD[topLevelConnectionDefinitions];
128183
delete inputTD[topLevelFormDefinitions];
129184
}

node/td-utils/test/expansion.test.suite.ts

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const positiveTestSuite = [
4040
"forms": [
4141
{
4242
"href": "props/prop2",
43+
"contentType": "application/json",
4344
},
4445
],
4546
},
@@ -86,7 +87,7 @@ export const positiveTestSuite = [
8687
"forms": [
8788
{
8889
"href": "coap://[2001:db8::1]/mything/props/prop2",
89-
"contentType": "application/cbor",
90+
"contentType": "application/json",
9091
"security": {
9192
"scheme": "nosec",
9293
},
@@ -125,7 +126,7 @@ export const positiveTestSuite = [
125126
},
126127
},
127128
{
128-
name: "cborcoap-alternate-input",
129+
name: "cborcoap-alternate-input-1",
129130
input: {
130131
"@context": "https://www.w3.org/ns/wot-next/td",
131132
"title": "recommended-test-cbor-default",
@@ -169,6 +170,50 @@ export const positiveTestSuite = [
169170
},
170171
},
171172
},
173+
{
174+
name: "cborcoap-alternate-input-2",
175+
input: {
176+
"@context": "https://www.w3.org/ns/wot-next/td",
177+
"title": "recommended-test-cbor-default",
178+
"form": {
179+
"contentType": "application/cbor",
180+
},
181+
"connection":{
182+
"base": "coap://[2001:DB8::1]/mything/",
183+
},
184+
"security": {
185+
"scheme": "nosec"
186+
},
187+
"properties": {
188+
"prop1": {
189+
"type": "string",
190+
"forms": [
191+
{
192+
"href": "props/prop1",
193+
},
194+
],
195+
},
196+
},
197+
},
198+
expected: {
199+
"@context": "https://www.w3.org/ns/wot-next/td",
200+
"title": "recommended-test-cbor-default",
201+
"properties": {
202+
"prop1": {
203+
"type": "string",
204+
"forms": [
205+
{
206+
"href": "coap://[2001:db8::1]/mything/props/prop1",
207+
"contentType": "application/cbor",
208+
"security": {
209+
"scheme": "nosec",
210+
},
211+
},
212+
],
213+
},
214+
},
215+
},
216+
},
172217
{
173218
name: "modbus-separate-form-connection",
174219
input: {
@@ -252,6 +297,125 @@ export const positiveTestSuite = [
252297
},
253298
},
254299
},
300+
{
301+
name: "multi-protocol-multi-contenttype",
302+
input: {
303+
"@context": "https://www.w3.org/ns/wot-next/td",
304+
"title": "recommended-test-multi-protocol",
305+
"connectionDefinitions": {
306+
"http": {
307+
"base": "http://192.168.1.10:8080/mything",
308+
},
309+
"coap": {
310+
"base": "coap://[2001:DB8::1]/mything",
311+
},
312+
},
313+
"formDefinitions": {
314+
"json": {
315+
"contentType": "application/json",
316+
},
317+
"cbor": {
318+
"contentType": "application/cbor",
319+
},
320+
},
321+
"form": ["json", "cbor"],
322+
"connection": ["http", "coap"],
323+
"security": {
324+
"scheme": "nosec",
325+
},
326+
"properties": {
327+
"prop1": {
328+
"type": "string",
329+
"forms": [
330+
{
331+
"href": "props/prop1",
332+
},
333+
],
334+
},
335+
"prop2": {
336+
"type": "string",
337+
"forms": [
338+
{
339+
"href": "props/prop2",
340+
},
341+
],
342+
},
343+
},
344+
},
345+
expected: {
346+
"@context": "https://www.w3.org/ns/wot-next/td",
347+
"title": "recommended-test-multi-protocol",
348+
349+
"properties": {
350+
"prop1": {
351+
"type": "string",
352+
"forms": [
353+
{
354+
"href": "http://192.168.1.10:8080/props/prop1",
355+
"contentType": "application/json",
356+
"security": {
357+
"scheme": "nosec",
358+
},
359+
},
360+
{
361+
"href": "coap://[2001:db8::1]/props/prop1",
362+
"contentType": "application/json",
363+
"security": {
364+
"scheme": "nosec",
365+
},
366+
},
367+
{
368+
"href": "http://192.168.1.10:8080/props/prop1",
369+
"contentType": "application/cbor",
370+
"security": {
371+
"scheme": "nosec",
372+
},
373+
},
374+
{
375+
"href": "coap://[2001:db8::1]/props/prop1",
376+
"contentType": "application/cbor",
377+
"security": {
378+
"scheme": "nosec",
379+
},
380+
},
381+
],
382+
},
383+
"prop2": {
384+
"type": "string",
385+
"forms": [
386+
{
387+
"href": "http://192.168.1.10:8080/props/prop2",
388+
"contentType": "application/json",
389+
"security": {
390+
"scheme": "nosec",
391+
},
392+
},
393+
{
394+
"href": "coap://[2001:db8::1]/props/prop2",
395+
"contentType": "application/json",
396+
"security": {
397+
"scheme": "nosec",
398+
},
399+
},
400+
{
401+
"href": "http://192.168.1.10:8080/props/prop2",
402+
"contentType": "application/cbor",
403+
"security": {
404+
"scheme": "nosec",
405+
},
406+
},
407+
{
408+
"href": "coap://[2001:db8::1]/props/prop2",
409+
"contentType": "application/cbor",
410+
"security": {
411+
"scheme": "nosec",
412+
},
413+
},
414+
],
415+
},
416+
},
417+
},
418+
},
255419
];
256420

257421
export const negativeTestSuite = [

node/td-utils/test/expansion.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("test negative examples", () => {
3131
try {
3232
const generatedTD = expandTD(t.input as any);
3333
} catch (error) {
34-
console.error("❌ Caught error:", (error as Error).message);
34+
console.error(`❌ ${t.name} Caught error:`, (error as Error).message);
3535
expect((error as Error).message).toBe(t.expected);
3636
}
3737
});

0 commit comments

Comments
 (0)