Skip to content
Open
10 changes: 10 additions & 0 deletions packages/core/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import rootConfig from "../../eslint.config.mjs";

export default [
...rootConfig,
{
rules: {
"@typescript-eslint/no-unnecessary-condition": "error",
},
},
];
7 changes: 2 additions & 5 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"build": "tsc -b",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"**/*.json\"",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"**/*.json\" \"*.mjs\"",
"test": "mocha --require ts-node/register --extension ts"
},
"bugs": {
Expand All @@ -50,8 +50,5 @@
"directories": {
"test": "test"
},
"keywords": [],
"eslintConfig": {
"extends": "../../.eslintrc.js"
}
"keywords": []
}
18 changes: 9 additions & 9 deletions packages/core/src/codecs/octetstream-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export default class OctetstreamCodec implements ContentCodec {
if (typeSem) {
if (typeSem[1] === "u") {
// compare with schema information
if (parameters?.signed === "true") {
if (parameters.signed === "true") {
throw new Error("Type is unsigned but 'signed' is true");
}
// no schema, but type is unsigned
Expand All @@ -117,7 +117,7 @@ export default class OctetstreamCodec implements ContentCodec {
dataType = typeSem[2];
if (parseInt(typeSem[3]) !== bitLength) {
throw new Error(
`Type is '${(typeSem[1] ?? "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` + bitLength
`Type is '${(typeSem[1] || "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` + bitLength
);
}
}
Expand All @@ -130,11 +130,11 @@ export default class OctetstreamCodec implements ContentCodec {
}

// Handle byte swapping
if (parameters?.byteSeq?.includes("BYTE_SWAP") === true && bytes.length > 1) {
if (parameters.byteSeq?.includes("BYTE_SWAP") === true && bytes.length > 1) {
bytes.swap16();
}

if (offset !== undefined && bitLength < bytes.length * 8) {
if (bitLength < bytes.length * 8) {
bytes = this.readBits(bytes, offset, bitLength);
bitLength = bytes.length * 8;
}
Expand Down Expand Up @@ -282,7 +282,7 @@ export default class OctetstreamCodec implements ContentCodec {
throw new Error("'ex:bitOffset' must be a non-negative number");
}

let dataType: string = schema?.type ?? undefined;
let dataType: string | undefined = schema?.type;

if (value === undefined) {
throw new Error("Undefined value");
Expand All @@ -300,7 +300,7 @@ export default class OctetstreamCodec implements ContentCodec {
if (typeSem) {
if (typeSem[1] === "u") {
// compare with schema information
if (parameters?.signed === "true") {
if (parameters.signed === "true") {
throw new Error("Type is unsigned but 'signed' is true");
}
// no schema, but type is unsigned
Expand All @@ -310,7 +310,7 @@ export default class OctetstreamCodec implements ContentCodec {
if (bitLength !== undefined) {
if (parseInt(typeSem[3]) !== bitLength) {
throw new Error(
`Type is '${(typeSem[1] ?? "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` +
`Type is '${(typeSem[1] || "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` +
bitLength
);
}
Expand Down Expand Up @@ -446,7 +446,7 @@ export default class OctetstreamCodec implements ContentCodec {
}
// Handle byte swapping

if (byteSeq?.includes("BYTE_SwAP") && byteLength > 1) {
if (byteSeq.includes("BYTE_SwAP") && byteLength > 1) {
buf.swap16();
}
switch (byteLength) {
Expand Down Expand Up @@ -584,7 +584,7 @@ export default class OctetstreamCodec implements ContentCodec {
parameters: { [key: string]: string | undefined } = {},
result?: Buffer | undefined
): Buffer {
if (typeof value !== "object" || value === null) {
if (typeof value !== "object") {
throw new Error("Value is not an object");
}

Expand Down
87 changes: 25 additions & 62 deletions packages/core/src/consumed-thing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ class InternalPropertySubscription extends InternalSubscription {
private readonly form: FormElementProperty
) {
super(thing, name, client);
const index = this.thing.properties?.[name].forms.indexOf(form as Form);
if (index === undefined || index < 0) {
const index = this.thing.properties[name].forms.indexOf(form as Form);
if (index < 0) {
throw new Error(`Could not find form ${form.href} in property ${name}`);
}
this.formIndex = index;
Expand All @@ -144,9 +144,6 @@ class InternalPropertySubscription extends InternalSubscription {

public async unobserveProperty(options: WoT.InteractionOptions = {}): Promise<void> {
const tp = this.thing.properties[this.name];
if (tp == null) {
throw new Error(`ConsumedThing '${this.thing.title}' does not have property ${this.name}`);
}
options.formIndex ??= this.matchingUnsubscribeForm();
const { form } = this.thing.getClientFor(tp.forms, "unobserveproperty", Affordance.PropertyAffordance, options);
if (form == null) {
Expand Down Expand Up @@ -195,7 +192,7 @@ class InternalPropertySubscription extends InternalSubscription {
for (let i = 0; i < forms.length; i++) {
let score = 0;
const form = forms[i];
if (form.op === operation || (form?.op?.includes(operation) === true && Array.isArray(form.op) === true)) {
if (form.op === operation || (form.op?.includes(operation) === true && Array.isArray(form.op) === true)) {
score += 1;
}

Expand Down Expand Up @@ -232,7 +229,7 @@ function findFormIndexWithScoring(
for (let i = 0; i < forms.length; i++) {
let score = 0;
const form = forms[i];
if (form.op === operation || (form?.op?.includes(operation) === true && Array.isArray(form.op) === true)) {
if (form.op === operation || (form.op?.includes(operation) === true && Array.isArray(form.op) === true)) {
score += 1;
}

Expand Down Expand Up @@ -261,8 +258,8 @@ class InternalEventSubscription extends InternalSubscription {
private readonly form: FormElementEvent
) {
super(thing, name, client);
const index = this.thing.events?.[name].forms.indexOf(form as Form);
if (index === undefined || index < 0) {
const index = this.thing.events[name].forms.indexOf(form as Form);
if (index < 0) {
throw new Error(`Could not find form ${form.href} in event ${name}`);
}
this.formIndex = index;
Expand All @@ -275,9 +272,6 @@ class InternalEventSubscription extends InternalSubscription {

public async unsubscribeEvent(options: WoT.InteractionOptions = {}): Promise<void> {
const te = this.thing.events[this.name];
if (te == null) {
throw new Error(`ConsumedThing '${this.thing.title}' does not have event ${this.name}`);
}

options.formIndex ??= this.matchingUnsubscribeForm();

Expand Down Expand Up @@ -461,7 +455,7 @@ export default class ConsumedThing extends Thing implements IConsumedThing {

let ws: SecurityScheme | undefined = this.securityDefinitions[s];
// also push nosec in case of proxy
if (ws?.scheme === "combo") {
if (ws.scheme === "combo") {
ws = resolveComboScheme(ws as ComboSecurityScheme, s);
}
if (ws != null) {
Expand All @@ -476,23 +470,21 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
}

ensureClientSecurity(client: ProtocolClient, form: Form | undefined): void {
if (this.securityDefinitions != null) {
const logStatement = () =>
debug(`ConsumedThing '${this.title}' setting credentials for ${client} based on thing security`);

if (form != null && Array.isArray(form.security) && form.security.length > 0) {
// Note security member in form objects overrides (i.e., completely replace) all definitions activated at the Thing level
// see https://www.w3.org/TR/wot-thing-description/#security-serialization-json

logStatement();
client.setSecurity(this.getSecuritySchemes(form.security), this.#servient.retrieveCredentials(this.id));
} else if (Array.isArray(this.security) && this.security.length > 0) {
logStatement();
client.setSecurity(
this.getSecuritySchemes(this.security as string[]),
this.#servient.getCredentials(this.id)
);
}
const logStatement = () =>
debug(`ConsumedThing '${this.title}' setting credentials for ${client} based on thing security`);

if (form != null && Array.isArray(form.security) && form.security.length > 0) {
// Note security member in form objects overrides (i.e., completely replace) all definitions activated at the Thing level
// see https://www.w3.org/TR/wot-thing-description/#security-serialization-json

logStatement();
client.setSecurity(this.getSecuritySchemes(form.security), this.#servient.retrieveCredentials(this.id));
} else if (Array.isArray(this.security) && this.security.length > 0) {
logStatement();
client.setSecurity(
this.getSecuritySchemes(this.security as string[]),
this.#servient.getCredentials(this.id)
);
}
}

Expand Down Expand Up @@ -566,17 +558,12 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
async readProperty(propertyName: string, options?: WoT.InteractionOptions): Promise<WoT.InteractionOutput> {
// TODO pass expected form op to getClientFor()
const tp = this.properties[propertyName];
if (tp == null) {
throw new Error(`ConsumedThing '${this.title}' does not have property ${propertyName}`);
}

const { client, form } = this.getClientFor(tp.forms, "readproperty", Affordance.PropertyAffordance, options);
if (form == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable form`);
}
if (client == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`);
}

debug(`ConsumedThing '${this.title}' reading ${form.href}`);

// uriVariables ?
Expand All @@ -597,7 +584,7 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
outputDataSchema: WoT.DataSchema | undefined
): InteractionOutput {
// infer media type from form if not in response metadata
content.type ??= form.contentType ?? "application/json";
content.type = form.contentType ?? "application/json";
// check if returned media type is the same as expected media type (from TD)
this.checkMediaTypeOrThrow(content, form);
return new InteractionOutput(content, form, outputDataSchema);
Expand All @@ -623,7 +610,7 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
synchronous?: boolean
): ActionInteractionOutput {
// infer media type from form if not in response metadata
content.type ??= form.contentType ?? "application/json";
content.type = form.contentType ?? "application/json";
// check if returned media type is the same as expected media type (from TD)
this.checkMediaTypeOrThrow(content, form);
return new ActionInteractionOutput(content, form, outputDataSchema, synchronous);
Expand Down Expand Up @@ -677,16 +664,10 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
): Promise<void> {
// TODO pass expected form op to getClientFor()
const tp = this.properties[propertyName];
if (tp == null) {
throw new Error(`ConsumedThing '${this.title}' does not have property ${propertyName}`);
}
const { client, form } = this.getClientFor(tp.forms, "writeproperty", Affordance.PropertyAffordance, options);
if (form == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable form`);
}
if (client == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`);
}
debug(`ConsumedThing '${this.title}' writing ${form.href} with '${value}'`);

const content = ContentManager.valueToContent(value, tp, form.contentType);
Expand Down Expand Up @@ -718,16 +699,10 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
options?: WoT.InteractionOptions
): Promise<WoT.ActionInteractionOutput> {
const ta = this.actions[actionName];
if (ta == null) {
throw new Error(`ConsumedThing '${this.title}' does not have action ${actionName}`);
}
const { client, form } = this.getClientFor(ta.forms, "invokeaction", Affordance.ActionAffordance, options);
if (form == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable form`);
}
if (client == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`);
}
debug(
`ConsumedThing '${this.title}' invoking ${form.href}${
parameter !== undefined ? " with '" + parameter + "'" : ""
Expand Down Expand Up @@ -763,16 +738,10 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
options?: WoT.InteractionOptions
): Promise<Subscription> {
const tp = this.properties[name];
if (tp == null) {
throw new Error(`ConsumedThing '${this.title}' does not have property ${name}`);
}
const { client, form } = this.getClientFor(tp.forms, "observeproperty", Affordance.PropertyAffordance, options);
if (form == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable form`);
}
if (client == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`);
}
if (this.observedProperties.has(name)) {
throw new Error(
`ConsumedThing '${this.title}' has already a function subscribed to ${name}. You can only observe once`
Expand Down Expand Up @@ -820,16 +789,10 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
options?: WoT.InteractionOptions
): Promise<Subscription> {
const te = this.events[name];
if (te == null) {
throw new Error(`ConsumedThing '${this.title}' does not have event ${name}`);
}
const { client, form } = this.getClientFor(te.forms, "subscribeevent", Affordance.EventAffordance, options);
if (form == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable form`);
}
if (client == null) {
throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`);
}
if (this.subscribedEvents.has(name)) {
throw new Error(
`ConsumedThing '${this.title}' has already a function subscribed to ${name}. You can only subscribe once`
Expand Down
20 changes: 8 additions & 12 deletions packages/core/src/content-serdes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ interface ReadContent {
* it can accept multiple serializers and decoders
*/
export class ContentSerdes {
private static instance: ContentSerdes;
private static instance: ContentSerdes | undefined;

public static readonly DEFAULT: string = "application/json";
public static readonly TD: string = "application/td+json";
Expand All @@ -57,7 +57,7 @@ export class ContentSerdes {
private offered: Set<string> = new Set<string>();

public static get(): ContentSerdes {
if (this.instance == null) {
if (!this.instance) {
this.instance = new ContentSerdes();
// JSON
this.instance.addCodec(new JsonCodec(), true);
Expand Down Expand Up @@ -126,14 +126,12 @@ export class ContentSerdes {
}

public contentToValue(content: ReadContent, schema: DataSchema): DataSchemaValue | undefined {
if (content.type === undefined) {
if (content.body.byteLength > 0) {
// default to application/json
content.type = ContentSerdes.DEFAULT;
} else {
// empty payload without media type -> void/undefined (note: e.g., empty payload with text/plain -> "")
return undefined;
}
if (content.body.byteLength > 0) {
// default to application/json
content.type = ContentSerdes.DEFAULT;
} else {
// empty payload without media type -> void/undefined (note: e.g., empty payload with text/plain -> "")
return undefined;
}

// split into media type and parameters
Expand Down Expand Up @@ -162,8 +160,6 @@ export class ContentSerdes {
schema: DataSchema | undefined,
contentType = ContentSerdes.DEFAULT
): Content {
if (value === undefined) warn("ContentSerdes valueToContent got no value");

if (value instanceof ReadableStream) {
return new Content(contentType, ProtocolHelpers.toNodeStream(value));
}
Expand Down
Loading
Loading