Skip to content
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

W3C compliance - Use forms instead of links in Properties, Actions, and Events #2811

Merged
merged 3 commits into from
Oct 13, 2021

Conversation

relu91
Copy link
Collaborator

@relu91 relu91 commented Mar 29, 2021

This PR covers the changes described in #2806 but it still does not tackle the open questions. It is merely a refactor of the current code base divided into small commits. I also introduced a test to verify the correct behavior of the Camera component in the Web UI.

The PR is still in the draft because we still need to understand how much this change will impact the addons and how to cope with the root level forms (for further details refer to #2806).

Note this change requires the updated version of https://github.com/WebThingsIO/gateway-addon-node in my forked repository. If you want to test this PR locally do the following:

  1. clone https://github.com/relu91/gateway-addon-node (be careful to clone the correct submodule: https://github.com/relu91/gateway-addon-ipc-schema)
  2. npm link
  3. cd your gateway folder
  4. npm link gateway-addon

@relu91 relu91 marked this pull request as draft March 29, 2021 08:46
@benfrancis
Copy link
Member

I'm having problems running this:

$ npm start

> [email protected] start /home/tola/Code/relu91/gateway
> npm run build && node build/app.js


> [email protected] build /home/tola/Code/relu91/gateway
> rm -rf build && cp -rL src build && find build -name '*.ts' -delete && tsc -p . && webpack

src/models/thing.ts:144:28 - error TS2571: Object is of type 'unknown'.

144           property.forms = property.forms.map((form) => {
                               ~~~~~~~~~~~~~~

src/models/thing.ts:144:48 - error TS7006: Parameter 'form' implicitly has an 'any' type.

144           property.forms = property.forms.map((form) => {
                                                   ~~~~

src/models/thing.ts:159:9 - error TS2571: Object is of type 'unknown'.

159         property.forms.push({
            ~~~~~~~~~~~~~~

src/models/thing.ts:229:24 - error TS2571: Object is of type 'unknown'.

229         action.forms = action.forms.map((form) => {
                           ~~~~~~~~~~~~

src/models/thing.ts:229:42 - error TS7006: Parameter 'form' implicitly has an 'any' type.

229         action.forms = action.forms.map((form) => {
                                             ~~~~

src/models/thing.ts:242:7 - error TS2571: Object is of type 'unknown'.

242       action.forms!.push({
          ~~~~~~~~~~~~~

src/models/thing.ts:255:23 - error TS2571: Object is of type 'unknown'.

255         event.forms = event.forms.map((form) => {
                          ~~~~~~~~~~~

src/models/thing.ts:255:40 - error TS7006: Parameter 'form' implicitly has an 'any' type.

255         event.forms = event.forms.map((form) => {
                                           ~~~~

src/models/thing.ts:268:7 - error TS2571: Object is of type 'unknown'.

268       event.forms.push({
          ~~~~~~~~~~~

src/models/thing.ts:635:28 - error TS2571: Object is of type 'unknown'.

635           property.forms = property.forms.map((form) => {
                               ~~~~~~~~~~~~~~

src/models/thing.ts:635:48 - error TS7006: Parameter 'form' implicitly has an 'any' type.

635           property.forms = property.forms.map((form) => {
                                                   ~~~~

src/models/thing.ts:650:9 - error TS2571: Object is of type 'unknown'.

650         property.forms.push({
            ~~~~~~~~~~~~~~

src/models/thing.ts:667:24 - error TS2571: Object is of type 'unknown'.

667         action.forms = action.forms.map((form) => {
                           ~~~~~~~~~~~~

src/models/thing.ts:667:42 - error TS7006: Parameter 'form' implicitly has an 'any' type.

667         action.forms = action.forms.map((form) => {
                                             ~~~~

src/models/thing.ts:680:7 - error TS2571: Object is of type 'unknown'.

680       action.forms!.push({
          ~~~~~~~~~~~~~

src/models/thing.ts:695:23 - error TS2571: Object is of type 'unknown'.

695         event.forms = event.forms.map((form) => {
                          ~~~~~~~~~~~

src/models/thing.ts:695:40 - error TS7006: Parameter 'form' implicitly has an 'any' type.

695         event.forms = event.forms.map((form) => {
                                           ~~~~

src/models/thing.ts:709:7 - error TS2571: Object is of type 'unknown'.

709       event.forms.push({
          ~~~~~~~~~~~

src/plugin/property-proxy.ts:85:12 - error TS2339: Property 'setForms' does not exist on type 'PropertyProxy'.

85       this.setForms(propertyDict.forms);
              ~~~~~~~~


Found 19 errors.

npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! [email protected] build: `rm -rf build && cp -rL src build && find build -name '*.ts' -delete && tsc -p . && webpack`
npm ERR! Exit status 2
npm ERR! 
npm ERR! Failed at the [email protected] build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/tola/.npm/_logs/2021-03-30T11_58_11_237Z-debug.log
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! [email protected] start: `npm run build && node build/app.js`
npm ERR! Exit status 2
npm ERR! 
npm ERR! Failed at the [email protected] start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/tola/.npm/_logs/2021-03-30T11_58_11_419Z-debug.log

These look like TypeScript errors to me (I'm new to TypeScript).

Maybe I didn't link the dependency correctly though?

@relu91
Copy link
Collaborator Author

relu91 commented Mar 30, 2021

Yep, it is our friendly typescript compiler. Did you build the gateway-addon? sorry I forgot to mention it, my bad.

@benfrancis
Copy link
Member

This code all looks good to me so far, thanks.

Not sure if @tim-hellhake wants to take a look from a TypeScript point of view since that's new to me?

In testing this I installed the virtual things adapter and noticed lots of type errors in the console like the one below:

2021-03-30 15:37:34.106 ERROR  : virtual-things-adapter: strict mode: missing type "object" for keyword "properties" at "https://raw.githubusercontent.com/WebThingsIO/gateway-addon-ipc-schema/master/messages/definitions.json#/allOf/1" (strictTypes)

I was able to add a virtual thing, but trying to set properties results in a 400 error with the payload

title parameter required

It looks like virtual-things-adapter might need to be updated :/

I haven't tested it with any other adapters yet.

@benfrancis
Copy link
Member

For the record, in addition to the steps above, I needed to build gateway-addon-node with a custom build command which doesn't do a submodule update and switch away from @relu91's w3c-compliance branch:
node generate-version.js && node generate-types.js && ./node_modules/typescript/bin/tsc -p .

before doing the npm link step.

@relu91
Copy link
Collaborator Author

relu91 commented May 7, 2021

Some updates on this PR. I tried to figure out the impact of changing links to forms at the IPC layer. Since the list of addons is quite long I tried a naive automated approach. Putting simply I cloned every single addons locally and searched for the string link. In this gists, you can find the script that I used to generate a report.

The report was then manually reviewed to eliminate false positives. So currently I find out 9 addons that should be updated after merging this PR. The addons marked as yes no were found not really impacted by the change and therefore discarded.

Since I am aware of the weaknesses of this process I would rate the different marks as follows:

  • no: very likely the project would not be affected -> it never uses the links property directly
  • yes no: it might be affected cause it contained usages of links. but most of the times they manipulated links at the root level
  • yes: something might break -> we should open an issue to warn the maintainers about the upcoming changes

@benfrancis
Copy link
Member

That's extremely useful, thank you @relu91.

So my understanding is that if we want to rename links to forms at the IPC layer as well as the web layer, the following add-ons would be impacted:

This seems like good news to me.

@flatsiedatsie @bewee @tim-hellhake would you be willing to rename "links" to "forms" in your add-ons before 2.0 is released?

We can make the change to the web layer only, but it would be much better to have it be consistent all the way down the stack.

We'd need to figure out an update strategy so that add-ons keep working while the upgrade is still rolling out.

@flatsiedatsie
Copy link
Contributor

Ofcourse, I'd be happy to.

Would the addons still work for people on the old version of the gateway is the addon started using the new names?

@relu91
Copy link
Collaborator Author

relu91 commented May 12, 2021

Would the addons still work for people on the old version of the gateway is the addon started using the new names?

I think it depends on addons maintainers. If in the logic of your addon you want to maintain backward compatibility you can easily check for links or forms and behave differently. There is no other change in the IPC layer so I think they will just continue to work.

But @benfrancis has the final word here :)

@benfrancis
Copy link
Member

check for links or forms and behave differently

If this is straightforward then it would be a good practice to recommend, so that add-ons can work with both 1.x and 2.x until 2.x becomes more widely adopted.

@tim-hellhake
Copy link
Member

@benfrancis Sure!

@tim-hellhake
Copy link
Member

@tim-hellhake
Copy link
Member

@relu91 @benfrancis
Would that be sufficient?

@relu91
Copy link
Collaborator Author

relu91 commented May 14, 2021

@tim-hellhake Thanks! let me update the dependency and see if the cli is ok with it 👍🏻

@relu91
Copy link
Collaborator Author

relu91 commented Jun 4, 2021

I tried to dig a little bit about why on node 10 we go a timeout in the tests. Locally, I can sometimes reproduce the issue. It seems that the virtual-things-adapter once installed it causes problems in the clean-up procedures. In particular, AFIK the addonManger.unistall is called before calling addonManager.unloadAddons. Since in the unistall we kill the process when virtual-things-adapter is unloaded, it also attempts to kill the process, but I think that there are everything blocks.

@codecov-commenter
Copy link

codecov-commenter commented Jul 1, 2021

Codecov Report

Merging #2811 (cc199c5) into master (78423ff) will increase coverage by 0.06%.
The diff coverage is 98.71%.

❗ Current head cc199c5 differs from pull request most recent head 4e8e206. Consider uploading reports for the commit 4e8e206 to get more accurate results
Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2811      +/-   ##
==========================================
+ Coverage   65.37%   65.43%   +0.06%     
==========================================
  Files         124      124              
  Lines        7971     7986      +15     
  Branches     1317     1315       -2     
==========================================
+ Hits         5211     5226      +15     
  Misses       2718     2718              
  Partials       42       42              
Impacted Files Coverage Δ
src/models/thing.ts 70.52% <98.41%> (+1.58%) ⬆️
src/constants.ts 100.00% <100.00%> (ø)
src/plugin/property-proxy.ts 91.11% <100.00%> (ø)
src/test/browser/page-object/thing-detail-page.ts 91.37% <100.00%> (+0.99%) ⬆️
src/controllers/new_things_controller.ts 43.58% <0.00%> (-2.57%) ⬇️
src/models/things.ts 75.46% <0.00%> (-1.23%) ⬇️
src/controllers/groups_controller.ts 27.00% <0.00%> (-0.73%) ⬇️
src/log-timestamps.ts 50.90% <0.00%> (+1.81%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 78423ff...4e8e206. Read the comment docs.

@benfrancis
Copy link
Member

Note: While I'm waiting for #2871 to land I've been continuing work on the links -> forms change in https://github.com/benfrancis/gateway/tree/links-to-forms which also includes those changes.

@relu91 relu91 force-pushed the fix_2806 branch 2 times, most recently from 2dfc982 to 0c8fe9b Compare September 29, 2021 17:27
@relu91 relu91 marked this pull request as ready for review September 29, 2021 17:27
Copy link
Member

@benfrancis benfrancis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of nits and I have one concern about how the forms are selected in the front end. I'm also going to suggest a follow-up to add backwards compatibility for Thing Descriptions which use links instead of forms so that we don't break existing add-ons. But otherwise I think this is very close to being landed!

src/models/thing.ts Outdated Show resolved Hide resolved
Comment on lines -238 to -247
if (action.links) {
action.links = action.links
.filter((link) => {
return link.rel && link.rel !== 'action';
})
.map((link) => {
if (link.proxy) {
delete link.proxy;
link.href = `${Constants.PROXY_PATH}/${encodeURIComponent(this.id)}${link.href}`;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following our discussions yesterday I'm going to suggest a follow-up to re-visit this later and add in backwards compatibility for links, by translating to forms at this point.

src/models/thing.ts Outdated Show resolved Hide resolved
static/js/models/thing-model.js Outdated Show resolved Hide resolved
static/js/models/thing-model.js Outdated Show resolved Hide resolved
static/js/rules/PropertySelect.js Outdated Show resolved Hide resolved
static/js/schema-impl/capability/thing.js Outdated Show resolved Hide resolved
static/js/schema-impl/capability/thing.js Outdated Show resolved Hide resolved
static/js/views/things.js Outdated Show resolved Hide resolved
@benfrancis
Copy link
Member

I've filed a follow-up issue regarding backwards-compatibility #2881

@benfrancis
Copy link
Member

I've also filed a follow-up for the WebSocket endpoint, which I've realised is currently still a link #2882

throw error;
}
})?.href;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file the right place to introduce this utility function?

I use the spread operator and the reverse function, although it might be not so efficient for a large forms array I think it is more expressive than a for loop that searches the form backward.

About the selection logic, tell me if you find it satisfactory. I choose to check also for protocol, maybe is an overkill? wot-adapter may publish TDs with other protocols in forms..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wanted to provide unit tests for this particular function, but it is not easy to test the frontend code so I gave up. Feel free to suggest how to properly test this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file the right place to introduce this utility function?

If it was only needed inside thing-model.js I would have put it there since this logic is quite specific to parsing a Thing Description, but since it's needed in multiple files I think it's reasonable to put it in this central utilities file.

I use the spread operator and the reverse function, although it might be not so efficient for a large forms array I think it is more expressive than a for loop that searches the form backward.
About the selection logic, tell me if you find it satisfactory. I choose to check also for protocol, maybe is an overkill? wot-adapter may publish TDs with other protocols in forms..

I think what I would have done is to just traverse the array forwards looking for matches and pick the last result, since there could theoretically be multiple matches, but I think your approach has the same effect. As far as I know the Thing Description specification doesn't specify what to do if there are multiple forms for the same operation using the same protocol.

I agree that in theory we should check the protocol, but if you're already making the assumption that the last matching form is an endpoint added by the gateway, then you could possibly also assume that it will be using http/https since that's all the gateway currently exposes.

@@ -216,7 +217,7 @@ class ThingModel extends Model {
if (typeof this.propertiesHref === 'undefined') {
const urls = Object.values(this.propertyDescriptions).map((v) => {
if (v.forms) {
return v.forms[0].href;
return Utils.selectFormHref(v.forms, Constants.WoTOperation.WRITE_PROPERTY);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for extensibility it might be right to look for writeproperty operation here even if now it is just reduntat.

break;
}
}
const href = Utils.selectFormHref(action.forms, Constants.WoTOperation.INVOKE_ACTION);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, I always used selectFormHref when looking for the gateway form.

Copy link
Member

@benfrancis benfrancis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, but I suggest either removing the protocol check or first resolving href against base before checking for http: or https:. (See further comments inline).

I think we could make this all more efficient by parsing the forms once on instantiation and storing a normalised model of the thing's affordances which has a URL endpoint per operation. Then we don't need to parse the forms every time we want to use them. However, this could be part of wider work to consolidate the Thing Description parsing logic and I'm happy to file a follow-up issue for that.

try {
const { protocol } = new URL(selectedForm.href);
return (
protocol === 'http:' &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be https:

throw error;
}
})?.href;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file the right place to introduce this utility function?

If it was only needed inside thing-model.js I would have put it there since this logic is quite specific to parsing a Thing Description, but since it's needed in multiple files I think it's reasonable to put it in this central utilities file.

I use the spread operator and the reverse function, although it might be not so efficient for a large forms array I think it is more expressive than a for loop that searches the form backward.
About the selection logic, tell me if you find it satisfactory. I choose to check also for protocol, maybe is an overkill? wot-adapter may publish TDs with other protocols in forms..

I think what I would have done is to just traverse the array forwards looking for matches and pick the last result, since there could theoretically be multiple matches, but I think your approach has the same effect. As far as I know the Thing Description specification doesn't specify what to do if there are multiple forms for the same operation using the same protocol.

I agree that in theory we should check the protocol, but if you're already making the assumption that the last matching form is an endpoint added by the gateway, then you could possibly also assume that it will be using http/https since that's all the gateway currently exposes.

);
} catch (error) {
if (error instanceof TypeError) {
// URL is relative or not well formatted
Copy link
Member

@benfrancis benfrancis Oct 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm right in saying that all the form hrefs added by the gateway are just paths rather than absolute URLs (e.g. /things/foo/properties/on). That means this logic would always be triggered and the protocol check would never be applied.

Rather than just ignore the protocol if not an absolute URL, I think if you're going to check the protocol what you need to do here is resolve the href against the base member of the Thing Description first.

This is probably strictly not necessary since the last form entry (added by the gateway) will always be HTTP, so we could just remove the protocol check if you prefer.

@relu91
Copy link
Collaborator Author

relu91 commented Oct 13, 2021

Thank you Ben I had the same concerns. I kept the protocol check, it feels more bulletproof to me but we can revisit later on. What do you of the current status?

I think we could make this all more efficient by parsing the forms once on instantiation and storing a normalised model of the thing's affordances which has a URL endpoint per operation. Then we don't need to parse the forms every time we want to use them. However, this could be part of wider work to consolidate the Thing Description parsing logic and I'm happy to file a follow-up issue for that.

Yes, I was tempted of doing the same but since this is already a pretty big change I wanted to not bring too much to the table. I follow-up PR would be perfect.

Copy link
Member

@benfrancis benfrancis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, thanks!

@benfrancis benfrancis merged commit 45e0015 into WebThingsIO:master Oct 13, 2021
@flatsiedatsie
Copy link
Contributor

I've been trying Gateway 1.1.0, and have run into this change. Some of my adons look for the links array.

I noticed that there is not a forms array. But the links array is also still there, just.. empty.

Perhaps the gateway could populate both the forms and links array? Then it would remain backwards compatible with these addons.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants