diff --git a/.dockerignore b/.dockerignore index 828da60ca..54e60c3b1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,10 @@ .git .vscode .idea/ +.nyc_output/ examples -**/node_modules +**/node_modules/ +node_modules/ +coverage/ +.*/ +tmp/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..dd139b4f6 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,43 @@ +module.exports = { + parser: "@typescript-eslint/parser", + parserOptions: { + tsconfigRootDir: __dirname, + project: ["./tsconfig.eslint.json"], + }, + extends: [ + "eslint:recommended", + "standard", + "prettier", + "plugin:@typescript-eslint/recommended", + "plugin:workspaces/recommended", + ], + plugins: ["@typescript-eslint", "unused-imports", "workspaces", "notice"], + env: { + es6: true, + node: true, + }, + ignorePatterns: [".eslintrc.js", "dist", "node_modules", "/examples", "bin", "*.js"], + rules: { + "notice/notice": [ + "error", + { + mustMatch: "Copyright \\(c\\) [0-9]{0,4} Contributors to the Eclipse Foundation", + templateFile: __dirname + "/license.template.txt", + onNonMatchingHeader: "replace", + }, + ], + "@typescript-eslint/no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": ["error"], + "@typescript-eslint/prefer-nullish-coalescing": "error", + "unused-imports/no-unused-imports": "error", + "@typescript-eslint/strict-boolean-expressions": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + args: "none", + varsIgnorePattern: "Test", // Ignore test suites from unused-imports + }, + ], + }, +}; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..e18545457 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,38 @@ +# Default owners (used as a fallback, last matching pattern takes the most precedence) +* @danielpeintner @relu91 + +# CoAP-Binding +/packages/binding-coap/ @JKRhb + +# HTTP-Binding +/packages/binding-http/ @danielpeintner @relu91 + +# Mbus-Binding +# /packages/binding-modbus/ + +# Modbus-Binding +/packages/binding-modbus/ @relu91 + +# MQTT-Binding +/packages/binding-mqtt/ @sebastiankb @egekorkan + +# NETCONF Binding +# /packages/binding-netconf/ + +# OPCUA Binding +# /packages/binding-opcua/ + +# WebSockets Binding +# /packages/binding-websockets/ + +# Browser Bundle +/packages/browser-bundle/ @danielpeintner @relu91 + +# CLI +/packages/browser-bundle/ @danielpeintner @relu91 @mkovatsc + +# Core +/packages/core/ @danielpeintner @relu91 @JKRhb + +# TD Tools +/packages/td-tools/ @danielpeintner @relu91 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..4e508d4e4 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,98 @@ +name: Default CI Pipeline + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + setup-and-test: + name: Tests (${{ matrix.os }}, Node ${{ matrix.node-version }}) + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + node-version: [18.x, 20.x] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.os }} ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + distribution: "temurin" + java-version: "11" + + # workaround for issue #1154 + - name: Install setuptools on MacOs + if: matrix.os == 'macos-latest' + run: | + sudo -H pip install setuptools + + - name: Install + run: npm ci + + - name: Build + run: npm run build + + # workaround for issue #1138 + - name: Add missing hostname to /etc/hosts + if: matrix.os == 'macos-latest' + run: | + sudo echo "127.0.0.1" `hostname` | sudo tee -a /etc/hosts + + - name: Test with coverage report + run: npm run coverage:only + timeout-minutes: 10 + + - name: Upload to codecov.io + uses: codecov/codecov-action@v2 + + eslint: + name: eslint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Use Node.js 18 + uses: actions/setup-node@v1 + with: + node-version: 18 + + - name: Install + run: npm ci + + - name: Lint + run: npm run lint + + version_consistency: + name: Check version consistency of packages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v18 + uses: actions/setup-node@v1 + with: + node-version: 18 + - name: verify packages version consistency accross sub-modules + run: npm run check:versions + + prettier: + name: Check coding style + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actionsx/prettier@v2 + with: + args: --check . diff --git a/.gitignore b/.gitignore index eb6db248a..4e1e4291d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,6 @@ jspm_packages # ignore dist dist/ -# ignore package-lock.json as not part of published module and might cause break -package-lock.json - # ignore compiled tests and scratch compiles test/**/*.js src/**/*.js @@ -56,25 +53,17 @@ packages/* !packages/binding-websockets !packages/binding-mqtt !packages/binding-modbus -!packages/binding-oracle -!packages/binding-fujitsu !packages/binding-opcua !packages/binding-netconf !packages/binding-modbus -!packages/binding-wotfirestore -!packages/binding-wotfirestore-browser-bundle +!packages/binding-mbus +!packages/binding-firestore !packages/cli !packages/browser-bundle !packages/examples -!packages/wot-agent -!packages/wot-console # JetBrains IDEs .idea/ -# hidetak -packages/wot-agent/clientScript.js -examples/.DS_Store -.DS_Store -packages/wot-console/public/binding-wotfirestore-bundle.min.js -packages/wot-console/public/wot-bundle.min.js +# tsc build info +tsconfig.tsbuildinfo diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..4e90fa3ea --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm run format:quick -- --staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 000000000..449fcdee1 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm test diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..93e7bc466 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +package-lock.json +**/dist +coverage +.nyc_output diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..8c7004572 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "printWidth": 120, + "singleQuote": false, + "tabWidth": 4, + "endOfLine": "lf" +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e9249ac3d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: node_js -os: - - linux - - osx - - windows -node_js: - - "node" - - "10" -# - "8" -# - "6.9" diff --git a/.vscode/launch.json b/.vscode/launch.json index 0b27c9408..8964977b0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,15 @@ "request": "launch", "name": "Launch Counter-Client", "program": "${workspaceRoot}/packages/cli/dist/cli.js", - "args": ["--clientonly", "counter-client.js"], + "args": ["--client-only", "counter-client.js"], + "cwd": "${workspaceRoot}/examples/scripts/" + }, + { + "type": "node", + "request": "launch", + "name": "Launch Counter", + "program": "${workspaceRoot}/packages/cli/dist/cli.js", + "args": ["counter.js"], "cwd": "${workspaceRoot}/examples/scripts/" }, { @@ -22,126 +30,162 @@ { "name": "Debug default servient", "request": "launch", - - "runtimeArgs": [ - "run-script", - "debug" - ], + + "runtimeArgs": ["run-script", "debug"], "port": 9229, "runtimeExecutable": "npm", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "node" }, + { + "name": "Test", + "request": "launch", + "runtimeArgs": ["run-script", "test:only"], + "port": 9229, + "runtimeExecutable": "npm", + "skipFiles": ["/**"], + "type": "pwa-node" + }, { "args": [ - "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${workspaceFolder}/packages/binding-http/test" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/binding-http/", "name": "HTTP Tests", - "program": "${workspaceFolder}/packages/binding-http/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${file}" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/binding-http/", "name": "HTTP Test current File", - "program": "${workspaceFolder}/packages/binding-http/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" + "skipFiles": ["/**"], + "type": "pwa-node" + }, + { + "args": [ + "-u", + "bdd", + "--require", + "ts-node/register", + "--timeout", + "999999", + "--colors", + "--extension", + "ts", + "${workspaceFolder}/packages/binding-modbus/test" ], - "type": "node" + "internalConsoleOptions": "openOnSessionStart", + "cwd": "${workspaceFolder}/packages/binding-modbus/", + "name": "Modbus Tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "request": "launch", + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "bdd", + "--require", + "ts-node/register", + "--timeout", + "999999", + "--colors", + "--extension", + "ts", + "${file}" + ], + "internalConsoleOptions": "openOnSessionStart", + "cwd": "${workspaceFolder}/packages/binding-modbus/", + "name": "Modbus Test current File", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "request": "launch", + "skipFiles": ["/**"], + "type": "pwa-node" + }, + { + "args": [ + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${workspaceFolder}/packages/binding-coap/test" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/binding-coap/", "name": "COAP Tests", - "program": "${workspaceFolder}/packages/binding-coap/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${file}" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/binding-coap/", "name": "COAP Test current File", - "program": "${workspaceFolder}/packages/binding-coap/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${workspaceFolder}/packages/binding-mqtt/test" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/binding-mqtt/", "name": "MQTT Tests", - "program": "${workspaceFolder}/packages/binding-mqtt/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "--require", + "--require", "ts-node/register", "--timeout", "999999", @@ -151,16 +195,14 @@ "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/binding-netconf/", "name": "NetConf Tests", - "program": "${workspaceFolder}/packages/binding-netconf/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "--require", + "--require", "ts-node/register", "--timeout", "999999", @@ -170,117 +212,125 @@ "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/binding-opcua/", "name": "Opcua Tests", - "program": "${workspaceFolder}/packages/binding-opcua/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${workspaceFolder}/packages/binding-websockets/test" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/binding-websockets/", "name": "Websockets Tests", - "program": "${workspaceFolder}/packages/binding-websockets/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${workspaceFolder}/packages/core/test" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/core/", "name": "Core Tests", - "program": "${workspaceFolder}/packages/core/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${file}" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/core/", "name": "Core Test File", - "program": "${workspaceFolder}/packages/core/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${workspaceFolder}/packages/td-tools/test" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/td-tools/", "name": "TDtools Tests", - "program": "${workspaceFolder}/packages/td-tools/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], - "type": "node" + "skipFiles": ["/**"], + "type": "pwa-node" }, { "args": [ - "-u", - "tdd", - "--compilers", - "ts:ts-node/register", + "--require", + "ts-node/register", "--timeout", "999999", "--colors", + "--extension", + "ts", "${file}" ], "internalConsoleOptions": "openOnSessionStart", "cwd": "${workspaceFolder}/packages/td-tools/", "name": "TDtools Test File", - "program": "${workspaceFolder}/packages/td-tools/node_modules/mocha/bin/_mocha", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" + "skipFiles": ["/**"], + "type": "pwa-node" + }, + { + "args": [ + "--require", + "ts-node/register", + "test/**/*.ts", + "--timeout", + "10000", + "--colors", + "--extension", + "ts", + "${workspaceFolder}/packages/binding-firestore/test" ], - "type": "node" + "internalConsoleOptions": "openOnSessionStart", + "cwd": "${workspaceFolder}/packages/binding-firestore/", + "name": "Firestore Tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "request": "launch", + "skipFiles": ["/**"], + "type": "pwa-node" } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 96460f763..0fb0cbb85 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,41 +1,19 @@ { "typescript.tsdk": "./node_modules/typescript/lib", "typescript.referencesCodeLens.enabled": true, - "fileHeaderComment.parameter":{ - "*":{ - "author": "the thingweb community", - "license_w3c":[ - "/********************************************************************************", - " * Copyright (c) 2018 - 2019 Contributors to the Eclipse Foundation", - " *", - " * See the NOTICE file(s) distributed with this work for additional", - " * information regarding copyright ownership.", - " *", - " * This program and the accompanying materials are made available under the", - " * terms of the Eclipse Public License v. 2.0 which is available at", - " * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and", - " * Document License (2015-05-13) which is available at", - " * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.", - " *", - " * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513", - " ********************************************************************************/" - ] - } -}, -"fileHeaderComment.template":{ - "*":[ - "${commentbegin}", - "${commentprefix} ${license_w3c}", - "${commentend}" - ] -}, - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/.DS_Store": true, - "src/**/*.js" : true, - "test/**/*.js" : true - } - -} \ No newline at end of file + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/.DS_Store": true, + "src/**/*.js": true, + "test/**/*.js": true + }, + "files.encoding": "utf8", + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "editor.tabSize": 4, + "editor.insertSpaces": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c0f70f704..8769bdd94 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,19 +1,24 @@ { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format - "version": "0.1.0", + "version": "2.0.0", "command": "npm", "args": ["run"], "echoCommand": true, - "isShellCommand": true, - "showOutput": "always", "problemMatcher": "$tsc", "tasks": [ { - "taskName": "build" + "label": "build", + "type": "shell", + "command": "npm", + "args": ["run", "build"], + "group": "build" }, { - "taskName": "test" + "label": "test", + "type": "shell", + "command": "npm", + "args": ["run", "test"] } ] -} \ No newline at end of file +} diff --git a/API.md b/API.md index 8abbc53cf..761ba12bd 100644 --- a/API.md +++ b/API.md @@ -1,43 +1,50 @@ ### What to do with the library + The two main functionalities of node-wot are creating WoT Things and interacting with other WoT Things. These can be combined into a Thing that interacts with other Things. #### Creating a WoT Thing + Creating a WoT Thing is called exposing a Thing. Exposing a Thing creates a Thing Description that can be used to by others to interact with this Thing. ##### Starting a Servient + ```javascript -WotCore = require("@node-wot/core") +WotCore = require("@node-wot/core"); let servient = new WotCore.Servient(); let WoT = await servient.start(); ``` ##### In Client mode, add factories + ```javascript -WotCore = require("@node-wot/core") -BindingHttp = require("@node-wot/binding-http") -BindingCoap = require("@node-wot/binding-coap") +WotCore = require("@node-wot/core"); +BindingHttp = require("@node-wot/binding-http"); +BindingCoap = require("@node-wot/binding-coap"); let servient = new NodeWoTCore.Servient(); -servient.addClientFactory(new BindingHttp.HttpClientFactory()) -servient.addClientFactory(new BindingCoap.CoapClientFactory()) +servient.addClientFactory(new BindingHttp.HttpClientFactory()); +servient.addClientFactory(new BindingCoap.CoapClientFactory()); ``` + The different bindings offer client factories. These need to be added in order to be able to access devices through this protocol. For more details on bindings, e.g. configuration options for a specific `*ClientFactory`, look at the `README.md` files in their respective directories. ##### In Server mode, add servers + ```javascript -WotCore = require("@node-wot/core") -CoapServer = require("@node-wot/binding-coap").CoapServer +WotCore = require("@node-wot/core"); +CoapServer = require("@node-wot/binding-coap").CoapServer; let servient = new WotCore.Servient(); servient.addServer(new CoapServer()); ``` -Same as for clients, bindings offer servers. +Same as for clients, bindings offer servers. ##### Credentials + ```javascript let servient = new (require("@node-wot/core")).Servient(); servient.addCredentials({ @@ -47,6 +54,7 @@ servient.addCredentials({ } ); ``` + You can add credentials like this. They are either used to authenticate clients when running in server mode or used to authenticate against servers when running in client mode. @@ -54,36 +62,40 @@ This example uses `username` and `password`, but other authentication mechanisms Authentication data is always mapped to a Thing through its id. ##### Expose a Thing + ```javascript let thing = WoT.produce({ - title: "counter", - description: "counter example Thing" + title: "counter", + description: "counter example Thing", }); // any other code to develop the Thing thing.expose(); ``` + Here, an object named `thing` is produced. At this stage, it has only a name and a description for humans to read. `thing.expose();` exposes/starts the exposed Thing in order to process external requests. This also creates a Thing Description that describes the interfaces of the `counter` thing. - ##### Add a Property definition to the Thing + Properties expose internal state of a Thing that can be directly accessed (get) and optionally manipulated (set). They are added as part of the `WoT.produce` invocation, like so: + ```javascript WoT.produce({ - title: "property", - properties: { - counter: { - type: "integer", - description: "current counter value", - observable: false - } - } + title: "property", + properties: { + counter: { + type: "integer", + description: "current counter value", + observable: false, + }, + }, }); ``` + This creates a Property. Its value can be initializes by calling `writeProperty`, otherwise it reads as `null`. After being written to, the value can be read by other Things. @@ -92,51 +104,52 @@ You can create a Property that has a more complex type, such as an object. This ```javascript WoT.produce({ - title: "complexproperty", - properties: { - color: { - type: "object", - properties: { - r: { type: "integer", minimum: 0, maximum: 255 }, - g: { type: "integer", minimum: 0, maximum: 255 }, - b: { type: "integer", minimum: 0, maximum: 255 }, - } - } - } + title: "complexproperty", + properties: { + color: { + type: "object", + properties: { + r: { type: "integer", minimum: 0, maximum: 255 }, + g: { type: "integer", minimum: 0, maximum: 255 }, + b: { type: "integer", minimum: 0, maximum: 255 }, + }, + }, + }, }); ``` ##### Add a Property read handler ```javascript -thing.setPropertyReadHandler( - "counter", - (propertyName) => { +thing.setPropertyReadHandler("counter", (propertyName) => { console.log("Handling read request for " + propertyName); return new Promise((resolve, reject) => { - resolve(Math.random(100)); - }) - }); + resolve(Math.random(100)); + }); +}); ``` + You can specify if the Thing needs to do something in case of a property read. Here, instead of reading a static value, a new random value is generated on every invocation. ##### Add a Property write handler ```javascript -thing.setPropertyWriteHandler('brightness', (value) => { - return new Promise((resolve, reject) => { - value %= 2; // only even values are valid in this example - setBrightness(value); - resolve(value); - }); +thing.setPropertyWriteHandler("brightness", (value) => { + return new Promise((resolve, reject) => { + value %= 2; // only even values are valid in this example + setBrightness(value); + resolve(value); + }); }); ``` + You can specify if the Thing needs do to something in case of a property write. Here, the value written is used to set the brightness of an LED that requires a specific function (`setBrightness()`) to do that. The property value becomes the value passed to `resolve()`, which in this case would mean the number modulo 2. ##### Add an Action definition to the Thing + Actions offer functions of the Thing. These functions may manipulate the interal state of a Thing in a way that is not possible through setting Properties. Examples are changing internal state that is not exposed as a Property, changing multiple Properties, changing Properties over time or with a process that shall not be disclosed. @@ -160,20 +173,23 @@ WoT.produce({ As can be seen above, `input` and `output` data types can be specified, similar to how property types are described in TDs. ##### Add an Action invoke handler + You need to write what will happen if an Action is invoked. This is done by setting an Action Handler: ```javascript thing.setActionHandler("increment", () => { - console.log("Incrementing"); - return thing.readProperty("counter").then((count) => { - let value = count + 1; - thing.writeProperty("counter", value); - }); + console.log("Incrementing"); + return thing.readProperty("counter").then((count) => { + let value = count + 1; + thing.writeProperty("counter", value); + }); }); - ``` +``` + Here, you see also how to access the properties of a Thing you are creating. ##### Add an Event definition to the Thing + The Event Interaction Affordance describes event sources that asynchronously push messages. This means that instead of communicating state, state transitions (events) are communicated (e.g. "clicked"). Events may be triggered by internal state changes that are not exposed as Properties. @@ -184,20 +200,21 @@ In the following, we will add the Event `onchange`: ```javascript WoT.produce({ - title: "change", - events: { - onchange: { - type: "number" - } - } + title: "change", + events: { + onchange: { + type: "number", + }, + }, }); ``` ##### Emit Event, i.e. notify all listeners subscribed to that Event + ```javascript -setInterval(async() => { - ++counter; - thing.emitEvent("onchange", counter); +setInterval(async () => { + ++counter; + thing.emitEvent("onchange", counter); }, 5000); ``` @@ -221,33 +238,39 @@ Here the event is triggered in regular intervals but emitting an event can be do * to update a Property value; * to run an Action: take the parameters from the request, execute the defined action, and return the result; --> - #### Interacting with another WoT Thing + Interacting with another WoT Thing is called consuming a Thing and works by using its Thing Description. ##### Fetch a Thing Description of a Thing given its URL + ```javascript WoTHelpers.fetch("http://localhost:8080/counter").then(async(td) => { // Do something with the TD } ``` + URLs can have various schemes, including `file://` to read from the local filesystem. ##### Consume a TD of a Thing, including parsing the TD and generating the protocol bindings in order to access lower level functionality + ```javascript -WoTHelpers.fetch("http://localhost:8080/counter").then(async(td) => { - let thing = WoT.consume(td); - // Do something with the consumed Thing +WoTHelpers.fetch("http://localhost:8080/counter").then(async (td) => { + let thing = WoT.consume(td); + // Do something with the consumed Thing }); ``` + Things can be `consume`d no matter if they were fetched with WoTHelpers or not. `consume` only requires a TD as an `Object`, so you could also use `fs.readFile` and `JSON.parse` or inline it into your code. As long at it results in a TD Object, you can receive it over Fax, Morse it or use smoke signals. #### On a consumed Thing + You can access all the interactions this Thing has and interact with them. ##### Read the value of a Property or set of properties + You can read the property values with the `readProperty` function. It is an asynchronous function that will take some time to complete. So you should handle it explicitely. @@ -259,25 +282,30 @@ console.info("count value is", read1); ``` ##### Set the value of a Property or a set of properties + You can write to a property by using the `writeProperty` function. ```javascript thing.writeProperty("color", { r: 255, g: 255, b: 0 }); ``` + ##### Invoke an Action + You can invoke an action by using the `invokeAction` function. It is an asynchronous function that will take some time to complete. So you should handle it explicitly. Here we use the `async`/`await` functionality of NodeJS. Declare the surrounding function as `async`, e.g., the `WoTHelpers.fetch()` resolve handler: + ```javascript WoTHelpers.fetch(myURI).then(async(td) => { ... }); ``` Use `await` to make Promises synchronous (blocking): + ```javascript await thing.invokeAction("increment"); // or passing a value diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..c2135ea58 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,47 @@ +# Community Code of Conduct + +**Version 1.2 +August 19, 2020** + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as community members, contributors, committers, and project leaders pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +With the support of the Eclipse Foundation staff (the “Staff”), project committers and leaders are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project committers and leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the Eclipse Foundation project or its community in public spaces. Examples of representing a project or community include posting via an official social media account, or acting as a project representative at an online or offline event. Representation of a project may be further defined and clarified by project committers, leaders, or the EMO. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Staff at codeofconduct@eclipse.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The Staff is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project committers or leaders who do not follow the Code of Conduct in good faith may face temporary or permanent repercussions as determined by the Staff. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1da2dd789..ec856dc02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for your interest in this project. General information regarding source code management, builds, coding standards, and more can be found here: -* https://projects.eclipse.org/projects/iot.thingweb/developer +- https://projects.eclipse.org/projects/iot.thingweb/developer ## Legal Requirements @@ -13,30 +13,190 @@ This process helps us in creating great open source software within a safe legal Thus, before your contribution can be accepted by the project team, contributors must electronically sign the [Eclipse Contributor Agreement (ECA)](http://www.eclipse.org/legal/ECA.php) and follow these preliminary steps: -* Obtain an [Eclipse Foundation account](https://accounts.eclipse.org/) - * Anyone who currently uses Eclipse Bugzilla or Gerrit systems already has one of those - * Newcomers can [create a new account](https://accounts.eclipse.org/user/register?destination=user) -* Add your GiHub username to your Eclipse Foundation account - * ([Log into Eclipse](https://accounts.eclipse.org/)) - * Go to the *Edit Profile* tab - * Fill in the *GitHub ID* under *Social Media Links* and save -* Sign the [Eclipse Contributor Agreement](http://www.eclipse.org/legal/ECA.php) - * ([Log into Eclipse](https://accounts.eclipse.org/)) - * If the *Status* entry *Eclipse Contributor Agreement* has a green checkmark, the ECA is already signed - * If not, go to the *Eclipse Contributor Agreement* tab or follow the corresponding link under *Status* - * Fill out the form and sign it electronically -* Sign-off every commit using the same email address used for your Eclipse account - * Set the Git user email address with `git config user.email ""` - * Add the `-s` flag when you make the commit(s), e.g. `git commit -s -m "feat: add support for magic"` -* Open a [Pull Request](https://github.com/eclipse/thingweb.node-wot/pulls) +- Obtain an [Eclipse Foundation account](https://accounts.eclipse.org/) + - Anyone who currently uses Eclipse Bugzilla or Gerrit systems already has one of those + - Newcomers can [create a new account](https://accounts.eclipse.org/user/register?destination=user) +- Add your GiHub username to your Eclipse Foundation account + - ([Log into Eclipse](https://accounts.eclipse.org/)) + - Go to the _Edit Profile_ tab + - Fill in the _GitHub ID_ under _Social Media Links_ and save +- Sign the [Eclipse Contributor Agreement](http://www.eclipse.org/legal/ECA.php) + - ([Log into Eclipse](https://accounts.eclipse.org/)) + - If the _Status_ entry _Eclipse Contributor Agreement_ has a green checkmark, the ECA is already signed + - If not, go to the _Eclipse Contributor Agreement_ tab or follow the corresponding link under _Status_ + - Fill out the form and sign it electronically +- Sign-off every commit using the same email address used for your Eclipse account + - Set the Git user email address with `git config user.email ""` + - Add the `-s` flag when you make the commit(s), e.g. `git commit -s -m "feat: add support for magic"` +- Open a [Pull Request](https://github.com/eclipse-thingweb/node-wot/pulls) For more information, please see the Eclipse Committer Handbook: https://www.eclipse.org/projects/handbook/#resources-commit +## Clone and build + +Clone the repository: + +``` +git clone https://github.com/eclipse-thingweb/node-wot +``` + +Go into the repository: + +``` +cd node-wot +``` + +Install root dependencies (locally installs tools such as [typescript](https://www.npmjs.com/package/typescript)): + +``` +npm ci +``` + +Use `tsc` to transcompile TS code to JS in the `dist` directory for each package: +_Note: This step automatically calls `npm run bootstrap`._ + +``` +npm run build +``` + +#### Optional steps + +###### Link Packages + +Make all packages available on your local machine (as symlinks). You can then use each package in its local version via `npm link ` instead of `npm install ` (see also https://docs.npmjs.com/cli/link). This is also useful if you want to test the CLI with a local script. + +``` +sudo npm run link +``` + +(On Windows omit `sudo`) + +##### Link Local wot-typescript-definitions + +When experimenting with new APIs, developers might need to change the [wot-typescript-definitions](https://www.npmjs.com/package/wot-typescript-definitions). If you want to use your own version of wot-types-defitions you can link your local copy using the following steps. +Use npm link for this as well: + +``` +git clone https://github.com/w3c/wot-scripting-api/ +cd wot-scripting-api/typescript/ +sudo npm link +``` + +(On Windows omit `sudo`) + +In each node-wot package, link the local version made available in the previous step: + +``` +sudo npm link wot-typescript-definitions +``` + +(On Windows omit `sudo`) + +##### Optimization + +To reduce the size of the installation from about 800 MByte down to about 200 MByte, you can run the following commands (currently only tested on Linux): +`npm prune --production` + +#### Troubleshooting + +- Build error about `No matching version found for @node-wot/...` or something about `match` + - try `npm run unlock` from the project root before building +- `sudo npm run link` does not work + - try `npm run unlock` from the project root before calling `[sudo] npm run link` + - try `npm link` in each package directory in this order: td-tools, core, binding-\*, cli, demo-servients +- Error mesage for `npm link @node-wot/` + `ELOOP: too many symbolic links encountered, stat '/usr/lib/node_modules/@node-wot/` + 1. Run `npm run link` in `thingweb.node-wot` again + 2. Remove `node_modules` in the targeted project + 3. Remove all `@node-wot/` dependencies in your `package.json` + 4. Run `npm i` again + 5. Install the packages with `npm link @node-wot/` +- Build error around `prebuild: npm run bootstrap` + - This has been seen failing on WSL. Try using a more recent Node.js version + +## Adding a New Protocol Binding + +In order to add support for a new protocol binding, you need to implement the protocol interfaces defined in the `core` package. +For the protocol to be usable via a `ConsumedThing` object, you need to implement the `ProtocolClientFactory` and the `ProtocolClient` interfaces. +For the protocol to be usable via an `ExposedThing` object, you need to implement the `ProtocolServer` interface. +The resulting `ProtocolClientFactory` and `ProtocolServer` implementations can then be used to enhance a given `Servient` with support for the protocol in question. + + + +In the following, we will give a couple of guidelines and examples for how to add specific features (such as logging) to your protocol binding implementation, keeping it consistent with the already existing packages. + +### Starting a New Binding Implementation + +`node-wot` is structured as a mono repo with a separate package (or "workspace") for each binding. +If you want to add a new package for a protocol called `foo`, you can use the following command for initialization in the repository's top-level directory: + +```sh +npm init -w ./packages/binding-foo +``` + +Since node-wot uses a single lock file (`package-lock.json`) for tracking all packages' dependencies, adding a new dependency to your binding also requires you to run the `npm install` command with the `-w` option. +For instance, if you need the `foobaz` package to implement your protocol binding, you can install it like so: + +```sh +npm install foobaz -w ./packages/binding-foo +``` + +In order to support linting and typescript transpilation, you should add both an `.eslintrc.json` and a `tsconfig.json` file to your package. +Examples for these can be found in the already existing binding packages. + + + +### Adding Logging Functionality + +Please use the `createLoggers` function from the `core` package for adding logging functionality to your package. +The function accepts an arbitrary number of arguments that will be mapped to `debug` namespaces. +In the example below, the `createLoggers` function will map its arguments `binding-foo` and `foo-server` to the namespace `node-wot:binding-foo:foo-server`. +The resulting functions `debug`, `info`, `warn`, and `error` will append their log-level to this namespace when creating the actual log message. +This enables filtering as described in the `README` section on logging. + +```ts +import { createLoggers } from "@node-wot/core"; +const { debug, info, warn, error } = createLoggers("binding-foo", "foo-server"); + +function startFoo() { + info("This is an info message!"); + debug("This is a debug message!"); + warn("This is a warn message!"); + error("This is an error message!"); +} +``` + +### Checking for `undefined` or `null` + +In node-wot, we enabled [strict boolean expressions](https://typescript-eslint.io/rules/strict-boolean-expressions/). In summary, this means that in the +the code base is not allowed to use non-boolean expressions where a boolean is expected (see the [examples](https://typescript-eslint.io/rules/strict-boolean-expressions/#examples)). +How then should the contributor deal with nullable variables? For example: + +```ts +function(arg1: string | null | undefined) { + // ERROR: not allowed by strict-boolean-expressions + if (!arg) { throw new Error("arg should be defined!); } +} +``` + +Instead of checking for both null and `undefined` values (`if (arg !== undefined && arg !== null)`) the preferred solution is to use `!=` or `==` operator. Interestingly in JavaScript +with `==`, `null` and `undefined` are equal to each other. Example: + +```ts +function(arg1: string | null | undefined) { + // OK + if (arg == null) { throw new Error("arg should be defined!); } +} +``` + +Further reading on the motivations can be found [here](https://basarat.gitbook.io/typescript/recap/null-undefined#checking-for-either). + ## Commits Eclipse Thingweb uses Conventional Changelog, which structure Git commit messages in a way that allows automatic generation of changelogs. Commit messages must be structured as follows: + ``` (): @@ -45,38 +205,41 @@ Commit messages must be structured as follows: