-
Notifications
You must be signed in to change notification settings - Fork 99
Support ID-based CoAP paths for exposed Things #1490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -67,6 +67,7 @@ export default class CoapServer implements ProtocolServer { | |||||||||||||||
|
|
||||||||||||||||
| private readonly port: number; | ||||||||||||||||
| private readonly address?: string; | ||||||||||||||||
| private readonly devFriendlyUri: boolean; | ||||||||||||||||
|
|
||||||||||||||||
| private mdnsIntroducer?: MdnsIntroducer; | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -84,6 +85,7 @@ export default class CoapServer implements ProtocolServer { | |||||||||||||||
| constructor(config?: CoapServerConfig) { | ||||||||||||||||
| this.port = config?.port ?? 5683; | ||||||||||||||||
| this.address = config?.address; | ||||||||||||||||
| this.devFriendlyUri = config?.devFriendlyUri ?? true; | ||||||||||||||||
|
|
||||||||||||||||
| // WoT-specific content formats | ||||||||||||||||
| registerFormat(ContentSerdes.JSON_LD, 2100); | ||||||||||||||||
|
|
@@ -143,34 +145,44 @@ export default class CoapServer implements ProtocolServer { | |||||||||||||||
|
|
||||||||||||||||
| public async expose(thing: ExposedThing, tdTemplate?: WoT.ExposedThingInit): Promise<void> { | ||||||||||||||||
| const port = this.getPort(); | ||||||||||||||||
| const urlPath = this.createThingUrlPath(thing); | ||||||||||||||||
| const paths = this.createThingUrlPaths(thing); | ||||||||||||||||
|
|
||||||||||||||||
| if (port === -1) { | ||||||||||||||||
| warn("CoapServer is assigned an invalid port, aborting expose process."); | ||||||||||||||||
| return; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| this.fillInBindingData(thing, port, urlPath); | ||||||||||||||||
| for (const urlPath of paths) { | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| this.fillInBindingData(thing, port, urlPath); | ||||||||||||||||
|
|
||||||||||||||||
| debug(`CoapServer on port ${port} exposes '${thing.title}' as unique '/${urlPath}'`); | ||||||||||||||||
| debug(`CoapServer on port ${port} exposes '${thing.title}' as unique '/${urlPath}'`); | ||||||||||||||||
|
|
||||||||||||||||
| this.setUpIntroductionMethods(thing, urlPath, port); | ||||||||||||||||
| this.setUpIntroductionMethods(thing, urlPath, port); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private createThingUrlPath(thing: ExposedThing) { | ||||||||||||||||
| let urlPath = slugify(thing.title, { lower: true }); | ||||||||||||||||
| private createThingUrlPaths(thing: ExposedThing): string[] { | ||||||||||||||||
| const paths: string[] = []; | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| // Title-based path | ||||||||||||||||
|
||||||||||||||||
| // Title-based path | |
| // Title-based path | |
| // Even when devFriendlyUri is false we still create a title-based path if the Thing has no ID. | |
| // This mirrors the HTTP binding behavior and ensures that Things without IDs can still be exposed | |
| // under a stable, discoverable path derived from their title. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment could rather be used as the basis for a doc comment for the function. Generally speaking, though, I don't think the mirroring of the HTTP binding needs to be mentioned that often.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. I was wondering whether we could end up in the following situation here:
- Starting point: two (partial) TDs have the same title, but only the second one has an ID
- For the second TD, a nameclash regarding the title is detected because the first TD did not have an ID and therefore an identical URL path
- The title of the second TD is appended with a
_2 - The urlPath of the second ID is further expanded by the ID
This is certainly an edge case (I am not actually not certain at the moment whether it is possible to have a missing ID at this point or whether that one is going to be set by the Servient), but I still wanted to mention it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| paths.push(urlPath); | |
| urlPaths.push(urlPath); |
Copilot
AI
Feb 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ID-based path registration doesn't check for collisions with existing paths. If a Thing's ID matches another Thing's title-based path, it will silently overwrite the existing registration. Consider adding collision detection for ID paths or at least logging a warning when an ID path overwrites an existing path, similar to how title-based paths handle collisions with the numeric suffix pattern.
| if (typeof thing.id === "string" && thing.id.length > 0) { | |
| if (typeof thing.id === "string" && thing.id.length > 0) { | |
| if (this.things.has(thing.id)) { | |
| warn( | |
| `CoapServer path collision detected: Thing with id '${thing.id}' will overwrite existing Thing registration at path '/${thing.id}'.` | |
| ); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, that is actually another interesting observation.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -30,6 +30,7 @@ export * from "./coaps-client"; | |||||||||||||||||||||||||
| export interface CoapServerConfig { | ||||||||||||||||||||||||||
| port?: number; | ||||||||||||||||||||||||||
| address?: string; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| address?: string; | |
| address?: string; | |
| /** | |
| * Controls whether title-based CoAP resource paths are generated. | |
| * | |
| * Similar to the HTTP binding, this option is `true` by default for backward | |
| * compatibility, so that Things are exposed using paths derived from their | |
| * titles. | |
| * | |
| * When set to `false` and a Thing has an ID, only ID-based paths are created | |
| * for that Thing. | |
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reference to the HTTP binding could be removed here.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -73,6 +73,42 @@ class CoapServerTest { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await coapServer.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @test async "should expose Thing with id and title and be reachable from both"() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const coapServer = new CoapServer({ port: PORT }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await coapServer.start(new Servient()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const testThing = new ExposedThing(new Servient(), { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "TestThing", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: "urn:dev:wot:test-thing-1234", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| properties: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| test: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "string", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| testThing.setPropertyReadHandler("test", async () => "OK"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-ignore | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| testThing.properties.test.forms = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await coapServer.expose(testThing); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uriByTitle = `coap://localhost:${coapServer.getPort()}/testthing/properties/test`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uriById = `coap://localhost:${coapServer.getPort()}/urn:dev:wot:test-thing-1234/properties/test`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const coapClient = new CoapClient(coapServer); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resp1 = await coapClient.readResource(new Form(uriByTitle)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect((await resp1.toBuffer()).toString()).to.equal('"OK"'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resp2 = await coapClient.readResource(new Form(uriById)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect((await resp2.toBuffer()).toString()).to.equal('"OK"'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await coapClient.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await coapServer.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @test async "should expose Thing with id and title and be reachable only by id when devFriendlyUri is false"() { | |
| const coapServer = new CoapServer({ port: PORT, devFriendlyUri: false }); | |
| await coapServer.start(new Servient()); | |
| const testThing = new ExposedThing(new Servient(), { | |
| title: "TestThing", | |
| id: "urn:dev:wot:test-thing-1234", | |
| properties: { | |
| test: { | |
| type: "string", | |
| }, | |
| }, | |
| }); | |
| testThing.setPropertyReadHandler("test", async () => "OK"); | |
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | |
| // @ts-ignore | |
| testThing.properties.test.forms = []; | |
| await coapServer.expose(testThing); | |
| const uriByTitle = `coap://localhost:${coapServer.getPort()}/testthing/properties/test`; | |
| const uriById = `coap://localhost:${coapServer.getPort()}/urn:dev:wot:test-thing-1234/properties/test`; | |
| const coapClient = new CoapClient(coapServer); | |
| // ID-based path should be reachable | |
| const respById = await coapClient.readResource(new Form(uriById)); | |
| expect((await respById.toBuffer()).toString()).to.equal('"OK"'); | |
| // Title-based path should not be registered when devFriendlyUri is false | |
| const respByTitle = await new Promise<IncomingMessage>((resolve) => { | |
| const req = request(uriByTitle); | |
| req.on("response", (res) => resolve(res)); | |
| req.end(); | |
| }); | |
| expect(respByTitle.code).to.equal("4.04"); | |
| await coapClient.stop(); | |
| await coapServer.stop(); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like a valid addition at first glance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.