From fa5a531f23456956d83787731c93b032f386b6df Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Thu, 20 Sep 2018 21:50:09 +0900 Subject: [PATCH 01/40] Support Web of Things Thing Description. Squashed commit of the following: commit f70b285ad65c3cbbd0209d1da5a1b07e7fd62bc7 Author: Kunihiko Toumura Date: Wed Sep 19 17:05:01 2018 +0900 Write notice in README commit dbb31b240c7012176c973b61e2820a3ae9e46f5a Merge: 5a0804d aabec5a Author: Kunihiko Toumura Date: Wed Sep 19 15:43:44 2018 +0900 merge upstream/master commit 5a0804d5bbece06024f591379a6641409678a751 Author: Kunihiko Toumura Date: Wed Sep 19 14:36:26 2018 +0900 remove special token handling and fix basic auth commit b7e8a00da7c5f894a36264b8be202a862f230530 Author: Kunihiko Toumura Date: Mon Jul 9 09:47:13 2018 +0900 status message changed commit 683ef00d897a431714657ac0a2b181037ea61b46 Author: Kunihiko Toumura Date: Sun Jul 1 10:59:58 2018 +0900 observe/event update commit 813578649f79efec60cafd1614415abc179b84db Author: Kunihiko Toumura Date: Sat Jun 30 17:11:29 2018 +0900 support "text/plain" payload for writing property. commit 6b5e4926c8ab2a5ffbbdeeee3d9701848941364d Author: Kunihiko Toumura Date: Sat Jun 30 16:42:38 2018 +0900 support more interaction patterns. commit 0d1ce54568e1b861a72c9d9496bf794b4c148762 Author: Kunihiko Toumura Date: Sat Jun 30 14:30:59 2018 +0900 First try for bundang. commit 9cf7cfd863e2bb991d5dd3854ba0a2eda2462b3a Author: Kunihiko Toumura Date: Mon Jun 25 09:46:58 2018 +0900 linted commit afd9ace9bdd871aba9d1bf10d71ee0f2e615a7b9 Author: Kunihiko Toumura Date: Fri Jun 22 22:22:22 2018 +0900 Update node label logic. commit c2a4e8d0647f741e537ef90cb2066922a1b1b15c Author: Kunihiko Toumura Date: Fri Jun 22 21:36:42 2018 +0900 Initial Commit --- .eslintrc.js | 32 +++ README.md | 16 ++ bin/node-red-nodegen.js | 8 + lib/nodegen.js | 126 ++++++++- templates/webofthings/LICENSE.mustache | 2 + templates/webofthings/README.md.mustache | 36 +++ templates/webofthings/node.html.mustache | 152 ++++++++++ templates/webofthings/node.js.mustache | 295 ++++++++++++++++++++ templates/webofthings/package.json.mustache | 27 ++ 9 files changed, 689 insertions(+), 5 deletions(-) create mode 100644 .eslintrc.js mode change 100644 => 100755 bin/node-red-nodegen.js create mode 100644 templates/webofthings/LICENSE.mustache create mode 100644 templates/webofthings/README.md.mustache create mode 100644 templates/webofthings/node.html.mustache create mode 100644 templates/webofthings/node.js.mustache create mode 100644 templates/webofthings/package.json.mustache diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..0a1653c --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,32 @@ +module.exports = { + "env": { + "es6": true + }, + "globals": {}, + "rules": { + "semi": [ + 2, + "always" + ], + "curly": 2, + "no-eq-null": 2, + "no-extend-native": 2, + "indent": [ + 2, + 4, + { + "SwitchCase": 1 + } + ], + "guard-for-in": 2, + "wrap-iife": [ + 2, + "any" + ], + "no-irregular-whitespace": 2, + "no-loop-func": 2, + "no-shadow": 0, + "dot-notation": 0, + "no-proto": 2 + } +}; diff --git a/README.md b/README.md index e3a7b64..7e3a48d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +___NOTICE: This branch (webofthings) is under heavy active development and not fully tested. +Not suitable for production.___ + # Node generator for Node-RED Node generator is command line tool to generate Node-RED node modules from several various sources including Swagger specification and function node's source. @@ -22,6 +25,7 @@ You may need to run this with `sudo`, or from within an Administrator command sh Supported source: - Function node (js file in library, "~/.node-red/lib/function/") - Swagger definition + - Thing Description of W3C Web of Things (jsonld file in library, or URL that points jsonld file) Options: -o : Destination path to save generated node (default: current directory) @@ -57,6 +61,18 @@ You may need to run this with `sudo`, or from within an Administrator command sh -> You can use lower-case node on Node-RED flow editor. +### Example 3. Create original node from Thing Description + +- node-red-nodegen example.jsonld +- cd node-red-contrib-example-thing +- sudo npm link +- cd ~/.node-red +- npm link node-red-contrib-example-thing +- node-red + +-> You can use Example Thing node on Node-RED flow editor. + + ## Documentation - [Use cases](docs/index.md#use-cases) ([Japanese](docs/index_ja.md#use-cases)) - [How to use Node generator](docs/index.md#how-to-use-node-generator) ([Japanese](docs/index_ja.md#how-to-use-node-generator)) diff --git a/bin/node-red-nodegen.js b/bin/node-red-nodegen.js old mode 100644 new mode 100755 index d94b208..b75359c --- a/bin/node-red-nodegen.js +++ b/bin/node-red-nodegen.js @@ -62,6 +62,7 @@ function help() { ' - Function node (js file in library, "~/.node-red/lib/function/")\n' + // ' - Subflow node (json file of subflow)\n' + ' - Swagger definition\n' + + ' - Thing Description (jsonld file)\n' + '\n' + 'Options:\n'.bold + ' -o : Destination path to save generated node (default: current directory)\n' + @@ -125,6 +126,13 @@ if (argv.help || argv.h) { }).catch(function (error) { console.log('Error: ' + error); }); + } else if (sourcePath.endsWith('.jsonld')) { + data.src = JSON.parse(fs.readFileSync(sourcePath)); + nodegen.wottd2node(data, options).then(function (result) { + console.log('Success: ' + result); + }).catch(function (error) { + console.log('Error: ' + error); + }); } else { console.error('error: Unsupported file type'); } diff --git a/lib/nodegen.js b/lib/nodegen.js index 4d8fb80..2c244b1 100644 --- a/lib/nodegen.js +++ b/lib/nodegen.js @@ -318,18 +318,18 @@ function swagger2node(data, options) { var isNotBodyParam = function () { return function (content, render) { return render('{{camelCaseName}}') !== 'body' ? render(content) : ''; - } + }; }; var isBodyParam = function () { return function (content, render) { return render('{{camelCaseName}}') === 'body' ? render(content) : ''; - } + }; }; var hasOptionalParams = function () { return function (content, render) { var params = render('{{#parameters}}{{^required}}{{camelCaseName}},{{/required}}{{/parameters}}'); return params.split(',').filter(p => p).some(p => p !== 'body') ? render(content) : ''; - } + }; }; var hasServiceParams = swagger.host === undefined || swagger.security !== undefined; @@ -395,7 +395,7 @@ function swagger2node(data, options) { beautify: false }); fs.writeFileSync(path.join(data.dst, data.module, 'locales', language, 'node.json'), - JSON.stringify(JSON.parse(languageFileSourceCode), null, 4)); + JSON.stringify(JSON.parse(languageFileSourceCode), null, 4)); }); // Create node_spec.js @@ -475,7 +475,123 @@ function swagger2node(data, options) { }); } +function wottd2node(data, options) { + return when.promise(function (resolve, reject) { + const td = data.src; + + // if name is not specified, use td.name for module name. + if (!data.name || data.name === '') { + // filtering out special characters + data.name = 'wot' + td.name.replace(/[^A-Za-z0-9]/g, '').toLowerCase(); + } + + if (data.module) { + if (data.prefix) { + reject('error: module name and prefix are conflicted'); + return; + } + } else { + if (data.prefix) { + data.module = data.prefix + data.name; + } else { + data.module = 'node-red-contrib-' + data.name; + } + } + + if (!data.version || data.version === '') { + data.version = '0.0.1'; + } + + data.tdstr = JSON.stringify(td); + data.properties = []; + const rwo = {}; + for (const p in td.properties) { // convert to array + if (td.properties.hasOwnProperty(p)) { + const q = td.properties[p]; + q.name = p; + if (!q.label || q.label === '') { + q.label = q.name; + } + data.properties.push(q); + rwo[p] = {}; + rwo[p].readable = true; + rwo[p].writable = q.writable; + rwo[p].observable = q.observable; + } + } + data.actions = []; + for (const a in td.actions) { + if (td.actions.hasOwnProperty(a)) { + const q = td.actions[a]; + q.name = a; + if (!q.label || q.label === '') { + q.label = q.name; + } + data.actions.push(q); + } + } + data.events = []; + for (const e in td.events) { + if (td.events.hasOwnProperty(e)) { + const q = td.events[e]; + q.name = e; + if (!q.label || q.label === '') { + q.label = q.name; + } + data.events.push(q); + } + } + + data.genproprwo = JSON.stringify(rwo); + + data.nodeName = data.name; + data.projectName = data.module; + data.projectVersion = data.version; + data.keywords = extractKeywords(data.keywords); + data.category = data.category || 'function'; + data.description = td.description; + data.licenseName = 'Apache-2.0'; + data.licenseUrl = ''; + data.links = td.links; + + createCommonFiles(path.join(__dirname, '../templates/webofthings'), data); + + // Create package.json + const packageTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/package.json.mustache'), 'utf-8'); + const packageSourceCode = mustache.render(packageTemplate, data); + fs.writeFileSync(path.join(data.dst, data.module, 'package.json'), packageSourceCode); + + // Create node.js + const nodeTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/node.js.mustache'), 'utf-8'); + const nodeSourceCode = mustache.render(nodeTemplate, data); + fs.writeFileSync(path.join(data.dst, data.module, 'node.js'), nodeSourceCode); + + // Create node.html + const htmlTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/node.html.mustache'), 'utf-8'); + const htmlSourceCode = mustache.render(htmlTemplate, data); + fs.writeFileSync(path.join(data.dst, data.module, 'node.html'), htmlSourceCode); + + // Create README.html + const readmeTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/README.md.mustache'), 'utf-8'); + const readmeSourceCode = mustache.render(readmeTemplate, data); + fs.writeFileSync(path.join(data.dst, data.module, 'README.md'), readmeSourceCode); + + // Create LICENSE + const licenseTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/LICENSE.mustache'), 'utf-8'); + const licenseSourceCode = mustache.render(licenseTemplate, data); + fs.writeFileSync(path.join(data.dst, data.module, 'LICENSE'), licenseSourceCode); + + if (options.tgz) { + runNpmPack(data); + resolve(path.join(data.dst, data.module + '-' + data.version + '.tgz')); + } else { + resolve(path.join(data.dst, data.module)); + } + }); +} + module.exports = { function2node: function2node, - swagger2node: swagger2node + swagger2node: swagger2node, + wottd2node: wottd2node }; diff --git a/templates/webofthings/LICENSE.mustache b/templates/webofthings/LICENSE.mustache new file mode 100644 index 0000000..02c35eb --- /dev/null +++ b/templates/webofthings/LICENSE.mustache @@ -0,0 +1,2 @@ +{{&licenseName}} +{{&licenseUrl}} diff --git a/templates/webofthings/README.md.mustache b/templates/webofthings/README.md.mustache new file mode 100644 index 0000000..7ee17fc --- /dev/null +++ b/templates/webofthings/README.md.mustache @@ -0,0 +1,36 @@ +{{projectName}} +===================== + +Node-RED node for {{nodeName}} + +{{description}} + +Install +------- + +Run the following command in your Node-RED user directory - typically `~/.node-red` + + npm install {{projectName}} + +Interactions +------------ + +### Properties + +{{#properties}} +- {{label}}: {{description}} +{{/properties}} + +### Actions + +{{#actions}} +- {{label}}: {{description}} +{{/actions}} + + +### Events + +{{#events}} +- {{label}}: {{description}} +{{/events}} + diff --git a/templates/webofthings/node.html.mustache b/templates/webofthings/node.html.mustache new file mode 100644 index 0000000..cd708f9 --- /dev/null +++ b/templates/webofthings/node.html.mustache @@ -0,0 +1,152 @@ + + + + + diff --git a/templates/webofthings/node.js.mustache b/templates/webofthings/node.js.mustache new file mode 100644 index 0000000..180c52c --- /dev/null +++ b/templates/webofthings/node.js.mustache @@ -0,0 +1,295 @@ +module.exports = function (RED) { + "use strict"; + const request = require("request"); + + function formSelection(interaction, forms) { + if (interaction === "property-read") { + for (const f of forms) { + if (f.rel && f.rel === "readProperty") { + return f; + } + } + return forms[0]; + } else if (interaction === "property-write") { + for (const f of forms) { + if (f.rel && f.rel === "writeProperty") { + return f; + } + } + return forms[0]; + } else if (interaction === "property-observe" || interaction === "event") { + for (const f of forms) { + if (f.rel && f.rel === "observeProperty") { + if (f.href && f.href.match(/^htt/)) { + return f; + } + } + if (f.subProtocol && f.subProtocol === "LongPoll") { + return f; + } + } + return forms[0]; + } else if (interaction === "action") { + return forms[0]; + } else { + return forms[0]; + } + } + + function {{className}}Node(config) { + RED.nodes.createNode(this, config); + const node = this; + node.interactiontype = config.interactiontype; + node.propname = config.propname; + node.proptype = config.proptype; + node.actname = config.actname; + node.evname = config.evname; + console.log(JSON.stringify(node)); + const username = node.credentials.username; + const password = node.credentials.password; + const token = node.credentials.token; + const auth = {}; + // if username and password is set, use it. + if (username && password) { + auth.user = username; + auth.pass = password; + auth.sendImmediately = true; + } else if (token) { + auth.bearer = token + auth.sendImmediately = true; + } + + node.td = JSON.parse(`{{&tdstr}}`); + if (node.interactiontype === "property") { + if (node.proptype === "read") { + node.on("input", (msg) => { + const prop = node.td.properties[node.propname]; + const options = {}; + // select a suitable form + const form = formSelection("property-read", prop.forms); + const href = form.href; + if (href.match(/^https?:\/\//)) { + options.uri = href; + } else { + options.uri = node.td.base + href; + } + options.rejectUnauthorized = false; + if (auth.user || auth.bearer) { + options.auth = auth; + } + options.method = form["http:methodName"] ? form["http:methodName"] : "GET"; + console.log(JSON.stringify(options)); + request(options, (err, res, body) => { + if (err) { + msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; + msg.statusCode = err.code; + node.send(msg); + } else { + msg.statusCode = res.statusCode; + msg.headers = res.headers; + msg.responseUrl = res.request.uri.href; + if (prop.forms[0].mediaType && + prop.forms[0].mediaType === "application/json") { + msg.payload = JSON.parse(body); + } else { + msg.payload = body; + } + // TODO: validation of return value + node.send(msg); + } + }); + }); + } else if (node.proptype === "write") { + node.on("input", (msg) => { + const prop = node.td.properties[node.propname]; + const options = {}; + const form = formSelection("property-write", prop.forms); + const href = form.href; + if (href.match(/^https?:\/\//)) { + options.uri = href; + } else { + options.uri = node.td.base + href; + } + options.rejectUnauthorized = false; + if (form.mediaType && + form.mediaType === "application/json") { + options.json = true; + } else { + options.json = false; + } + options.body = msg.payload; + if (auth.user || auth.bearer) { + options.auth = auth; + } + options.method = form["http:methodName"] ? form["http:methodName"] : "PUT"; + console.log(JSON.stringify(options)); + request(options, (err, res, body) => { + if (err) { + msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; + msg.statusCode = err.code; + node.send(msg); + } else { + msg.statusCode = res.statusCode; + msg.headers = res.headers; + msg.responseUrl = res.request.uri.href; + msg.payload = body; + // TODO: validation of return value + node.send(msg); + } + }); + }); + } else if (node.proptype === "observe") { + // long polling only + // use input as trigger of request + let timerId; + node.on("input", (msg) => { + const prop = node.td.properties[node.propname]; + const options = {}; + const form = formSelection("property-observe", prop.forms); + const href = form.href; + if (href.match(/^https?:\/\//)) { + options.uri = href; + } else { + options.uri = node.td.base + href; + } + options.rejectUnauthorized = false; + options.timeout = 60000; + if (auth.user || auth.bearer) { + options.auth = auth; + } + options.method = form["http:methodName"] ? form["http:methodName"] : "GET"; + console.log(JSON.stringify(options)); + request(options, (err, res, body) => { + if (err) { + msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; + msg.statusCode = err.code; + node.status({fill:"yellow",shape:"dot",text:"Polling timeout"}); + node.send(null); + timerId = setTimeout(() => { + node.emit("input",{}); + }, 5000); + } else { + node.status({fill:"green",shape:"dot",text:"OK"}); + msg.statusCode = res.statusCode; + msg.headers = res.headers; + msg.responseUrl = res.request.uri.href; + if (form.mediaType && + form.mediaType === "application/json") { + msg.payload = JSON.parse(body); + } else { + msg.payload = body; + } + // TODO: validation of return value + node.send(msg); + timerId = setTimeout(() => { + node.emit("input",{}); + }, 5000); + } + }); + }); + node.on("close", () => { + if (timerId) { + clearTimeout(timerId); + } + }); + node.emit("input",{}); + } + } else if (node.interactiontype === "action") { + node.on("input", (msg) => { + const act = node.td.actions[node.actname]; + const options = {}; + const form = formSelection("action", act.forms); + const href = form.href; + if (href.match(/^https?:\/\//)) { + options.uri = href; + } else { + options.uri = node.td.base + href; + } + options.rejectUnauthorized = false; + options.json = true; + options.body = msg.payload; + if (auth.user || auth.bearer) { + options.auth = auth; + } + options.method = form["http:methodName"] ? form["http:methodName"] : "POST"; + console.log(JSON.stringify(options)); + request(options, (err, res, body) => { + if (err) { + msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; + msg.statusCode = err.code; + node.send(msg); + } else { + msg.statusCode = res.statusCode; + msg.headers = res.headers; + msg.responseUrl = res.request.uri.href; + msg.payload = body; + // TODO: validation of return value + node.send(msg); + } + }); + }); + } else if (node.interactiontype === "event") { + // long polling only + // use input as trigger of request + let timerId; + node.on("input", (msg) => { + const ev = node.td.events[node.evname]; + const options = {}; + const form = formSelection("event", ev.forms); + const href = form.href; + if (href.match(/^https?:\/\//)) { + options.uri = href; + } else { + options.uri = node.td.base + href; + } + options.rejectUnauthorized = false; + options.timeout = 100000; + if (auth.user || auth.bearer) { + options.auth = auth; + } + options.method = form["http:methodName"] ? form["http:methodName"] : "GET"; + console.log(JSON.stringify(options)); + request(options, (err, res, body) => { + if (err) { + msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; + msg.statusCode = err.code; + node.status({fill:"yellow",shape:"dot",text:"Polling timeout"}); + node.send(null); + timerId = setTimeout(() => { + node.emit("input",{}); + }, 5000); + } else { + node.status({fill:"green",shape:"dot",text:"OK"}); + msg.statusCode = res.statusCode; + msg.headers = res.headers; + msg.responseUrl = res.request.uri.href; + if (form.mediaType && + form.mediaType === "application/json") { + msg.payload = JSON.parse(body); + } else { + msg.payload = body; + } + // TODO: validation of return value + node.send(msg); + timerId = setTimeout(() => { + node.emit("input",{}); + }, 5000); + } + }); + }); + node.on("close", () => { + if (timerId) { + clearTimeout(timerId); + } + }); + node.emit("input", {}); + } + }; + RED.nodes.registerType("{{&nodeName}}", {{&className}}Node, { + credentials: { + token: {type:"password"}, + username: {type:"text"}, + password: {type:"password"} + } + }); +}; \ No newline at end of file diff --git a/templates/webofthings/package.json.mustache b/templates/webofthings/package.json.mustache new file mode 100644 index 0000000..d724032 --- /dev/null +++ b/templates/webofthings/package.json.mustache @@ -0,0 +1,27 @@ +{ + "name": "{{&projectName}}", + "version": "{{&projectVersion}}", + "description": "Node-RED node for {{&nodeName}}", + "main": "node.js", + "scripts": { + "test": "mocha \"test/**/*_spec.js\"" + }, + "node-red": { + "nodes": { + "{{&nodeName}}": "node.js" + } + }, + "keywords": [ + {{#keywords}} + "{{name}}"{{^last}}, {{/last}} + {{/keywords}} + ], + "dependencies": { + "request": "^2.87.0" + }, + "devDependencies": { + "node-red": "0.18.7", + "node-red-node-test-helper": "0.1.8" + }, + "license": "{{&licenseName}}" +} \ No newline at end of file From 251507564a109ccd57b1971aaa295941accf06cc Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Tue, 25 Sep 2018 19:26:31 +0900 Subject: [PATCH 02/40] Fix observe logic. --- templates/webofthings/node.js.mustache | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/templates/webofthings/node.js.mustache b/templates/webofthings/node.js.mustache index 180c52c..ad6df79 100644 --- a/templates/webofthings/node.js.mustache +++ b/templates/webofthings/node.js.mustache @@ -141,7 +141,8 @@ module.exports = function (RED) { } else if (node.proptype === "observe") { // long polling only // use input as trigger of request - let timerId; + let timerId = null; + let reqObj = null; node.on("input", (msg) => { const prop = node.td.properties[node.propname]; const options = {}; @@ -159,7 +160,7 @@ module.exports = function (RED) { } options.method = form["http:methodName"] ? form["http:methodName"] : "GET"; console.log(JSON.stringify(options)); - request(options, (err, res, body) => { + reqObj = request(options, (err, res, body) => { if (err) { msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; msg.statusCode = err.code; @@ -190,7 +191,11 @@ module.exports = function (RED) { node.on("close", () => { if (timerId) { clearTimeout(timerId); - } + timerId = null; + } + if (reqObj) { + reqObj.abort(); + } }); node.emit("input",{}); } From 10be83b8f4cb4412bc40a53dbd3ce78d0e5145e5 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Tue, 25 Sep 2018 19:36:53 +0900 Subject: [PATCH 03/40] Revert "Fix observe logic." This reverts commit 251507564a109ccd57b1971aaa295941accf06cc. --- templates/webofthings/node.js.mustache | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/templates/webofthings/node.js.mustache b/templates/webofthings/node.js.mustache index ad6df79..180c52c 100644 --- a/templates/webofthings/node.js.mustache +++ b/templates/webofthings/node.js.mustache @@ -141,8 +141,7 @@ module.exports = function (RED) { } else if (node.proptype === "observe") { // long polling only // use input as trigger of request - let timerId = null; - let reqObj = null; + let timerId; node.on("input", (msg) => { const prop = node.td.properties[node.propname]; const options = {}; @@ -160,7 +159,7 @@ module.exports = function (RED) { } options.method = form["http:methodName"] ? form["http:methodName"] : "GET"; console.log(JSON.stringify(options)); - reqObj = request(options, (err, res, body) => { + request(options, (err, res, body) => { if (err) { msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; msg.statusCode = err.code; @@ -191,11 +190,7 @@ module.exports = function (RED) { node.on("close", () => { if (timerId) { clearTimeout(timerId); - timerId = null; - } - if (reqObj) { - reqObj.abort(); - } + } }); node.emit("input",{}); } From 7e68fe43322aa5668abb5ed0b29782f949f4bba7 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Tue, 25 Sep 2018 19:37:32 +0900 Subject: [PATCH 04/40] Revert "Support Web of Things Thing Description." This reverts commit fa5a531f23456956d83787731c93b032f386b6df. --- .eslintrc.js | 32 --- README.md | 16 -- bin/node-red-nodegen.js | 8 - lib/nodegen.js | 126 +-------- templates/webofthings/LICENSE.mustache | 2 - templates/webofthings/README.md.mustache | 36 --- templates/webofthings/node.html.mustache | 152 ---------- templates/webofthings/node.js.mustache | 295 -------------------- templates/webofthings/package.json.mustache | 27 -- 9 files changed, 5 insertions(+), 689 deletions(-) delete mode 100644 .eslintrc.js mode change 100755 => 100644 bin/node-red-nodegen.js delete mode 100644 templates/webofthings/LICENSE.mustache delete mode 100644 templates/webofthings/README.md.mustache delete mode 100644 templates/webofthings/node.html.mustache delete mode 100644 templates/webofthings/node.js.mustache delete mode 100644 templates/webofthings/package.json.mustache diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 0a1653c..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - "env": { - "es6": true - }, - "globals": {}, - "rules": { - "semi": [ - 2, - "always" - ], - "curly": 2, - "no-eq-null": 2, - "no-extend-native": 2, - "indent": [ - 2, - 4, - { - "SwitchCase": 1 - } - ], - "guard-for-in": 2, - "wrap-iife": [ - 2, - "any" - ], - "no-irregular-whitespace": 2, - "no-loop-func": 2, - "no-shadow": 0, - "dot-notation": 0, - "no-proto": 2 - } -}; diff --git a/README.md b/README.md index 7e3a48d..e3a7b64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -___NOTICE: This branch (webofthings) is under heavy active development and not fully tested. -Not suitable for production.___ - # Node generator for Node-RED Node generator is command line tool to generate Node-RED node modules from several various sources including Swagger specification and function node's source. @@ -25,7 +22,6 @@ You may need to run this with `sudo`, or from within an Administrator command sh Supported source: - Function node (js file in library, "~/.node-red/lib/function/") - Swagger definition - - Thing Description of W3C Web of Things (jsonld file in library, or URL that points jsonld file) Options: -o : Destination path to save generated node (default: current directory) @@ -61,18 +57,6 @@ You may need to run this with `sudo`, or from within an Administrator command sh -> You can use lower-case node on Node-RED flow editor. -### Example 3. Create original node from Thing Description - -- node-red-nodegen example.jsonld -- cd node-red-contrib-example-thing -- sudo npm link -- cd ~/.node-red -- npm link node-red-contrib-example-thing -- node-red - --> You can use Example Thing node on Node-RED flow editor. - - ## Documentation - [Use cases](docs/index.md#use-cases) ([Japanese](docs/index_ja.md#use-cases)) - [How to use Node generator](docs/index.md#how-to-use-node-generator) ([Japanese](docs/index_ja.md#how-to-use-node-generator)) diff --git a/bin/node-red-nodegen.js b/bin/node-red-nodegen.js old mode 100755 new mode 100644 index b75359c..d94b208 --- a/bin/node-red-nodegen.js +++ b/bin/node-red-nodegen.js @@ -62,7 +62,6 @@ function help() { ' - Function node (js file in library, "~/.node-red/lib/function/")\n' + // ' - Subflow node (json file of subflow)\n' + ' - Swagger definition\n' + - ' - Thing Description (jsonld file)\n' + '\n' + 'Options:\n'.bold + ' -o : Destination path to save generated node (default: current directory)\n' + @@ -126,13 +125,6 @@ if (argv.help || argv.h) { }).catch(function (error) { console.log('Error: ' + error); }); - } else if (sourcePath.endsWith('.jsonld')) { - data.src = JSON.parse(fs.readFileSync(sourcePath)); - nodegen.wottd2node(data, options).then(function (result) { - console.log('Success: ' + result); - }).catch(function (error) { - console.log('Error: ' + error); - }); } else { console.error('error: Unsupported file type'); } diff --git a/lib/nodegen.js b/lib/nodegen.js index 2c244b1..4d8fb80 100644 --- a/lib/nodegen.js +++ b/lib/nodegen.js @@ -318,18 +318,18 @@ function swagger2node(data, options) { var isNotBodyParam = function () { return function (content, render) { return render('{{camelCaseName}}') !== 'body' ? render(content) : ''; - }; + } }; var isBodyParam = function () { return function (content, render) { return render('{{camelCaseName}}') === 'body' ? render(content) : ''; - }; + } }; var hasOptionalParams = function () { return function (content, render) { var params = render('{{#parameters}}{{^required}}{{camelCaseName}},{{/required}}{{/parameters}}'); return params.split(',').filter(p => p).some(p => p !== 'body') ? render(content) : ''; - }; + } }; var hasServiceParams = swagger.host === undefined || swagger.security !== undefined; @@ -395,7 +395,7 @@ function swagger2node(data, options) { beautify: false }); fs.writeFileSync(path.join(data.dst, data.module, 'locales', language, 'node.json'), - JSON.stringify(JSON.parse(languageFileSourceCode), null, 4)); + JSON.stringify(JSON.parse(languageFileSourceCode), null, 4)); }); // Create node_spec.js @@ -475,123 +475,7 @@ function swagger2node(data, options) { }); } -function wottd2node(data, options) { - return when.promise(function (resolve, reject) { - const td = data.src; - - // if name is not specified, use td.name for module name. - if (!data.name || data.name === '') { - // filtering out special characters - data.name = 'wot' + td.name.replace(/[^A-Za-z0-9]/g, '').toLowerCase(); - } - - if (data.module) { - if (data.prefix) { - reject('error: module name and prefix are conflicted'); - return; - } - } else { - if (data.prefix) { - data.module = data.prefix + data.name; - } else { - data.module = 'node-red-contrib-' + data.name; - } - } - - if (!data.version || data.version === '') { - data.version = '0.0.1'; - } - - data.tdstr = JSON.stringify(td); - data.properties = []; - const rwo = {}; - for (const p in td.properties) { // convert to array - if (td.properties.hasOwnProperty(p)) { - const q = td.properties[p]; - q.name = p; - if (!q.label || q.label === '') { - q.label = q.name; - } - data.properties.push(q); - rwo[p] = {}; - rwo[p].readable = true; - rwo[p].writable = q.writable; - rwo[p].observable = q.observable; - } - } - data.actions = []; - for (const a in td.actions) { - if (td.actions.hasOwnProperty(a)) { - const q = td.actions[a]; - q.name = a; - if (!q.label || q.label === '') { - q.label = q.name; - } - data.actions.push(q); - } - } - data.events = []; - for (const e in td.events) { - if (td.events.hasOwnProperty(e)) { - const q = td.events[e]; - q.name = e; - if (!q.label || q.label === '') { - q.label = q.name; - } - data.events.push(q); - } - } - - data.genproprwo = JSON.stringify(rwo); - - data.nodeName = data.name; - data.projectName = data.module; - data.projectVersion = data.version; - data.keywords = extractKeywords(data.keywords); - data.category = data.category || 'function'; - data.description = td.description; - data.licenseName = 'Apache-2.0'; - data.licenseUrl = ''; - data.links = td.links; - - createCommonFiles(path.join(__dirname, '../templates/webofthings'), data); - - // Create package.json - const packageTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/package.json.mustache'), 'utf-8'); - const packageSourceCode = mustache.render(packageTemplate, data); - fs.writeFileSync(path.join(data.dst, data.module, 'package.json'), packageSourceCode); - - // Create node.js - const nodeTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/node.js.mustache'), 'utf-8'); - const nodeSourceCode = mustache.render(nodeTemplate, data); - fs.writeFileSync(path.join(data.dst, data.module, 'node.js'), nodeSourceCode); - - // Create node.html - const htmlTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/node.html.mustache'), 'utf-8'); - const htmlSourceCode = mustache.render(htmlTemplate, data); - fs.writeFileSync(path.join(data.dst, data.module, 'node.html'), htmlSourceCode); - - // Create README.html - const readmeTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/README.md.mustache'), 'utf-8'); - const readmeSourceCode = mustache.render(readmeTemplate, data); - fs.writeFileSync(path.join(data.dst, data.module, 'README.md'), readmeSourceCode); - - // Create LICENSE - const licenseTemplate = fs.readFileSync(path.join(__dirname, '../templates/webofthings/LICENSE.mustache'), 'utf-8'); - const licenseSourceCode = mustache.render(licenseTemplate, data); - fs.writeFileSync(path.join(data.dst, data.module, 'LICENSE'), licenseSourceCode); - - if (options.tgz) { - runNpmPack(data); - resolve(path.join(data.dst, data.module + '-' + data.version + '.tgz')); - } else { - resolve(path.join(data.dst, data.module)); - } - }); -} - module.exports = { function2node: function2node, - swagger2node: swagger2node, - wottd2node: wottd2node + swagger2node: swagger2node }; diff --git a/templates/webofthings/LICENSE.mustache b/templates/webofthings/LICENSE.mustache deleted file mode 100644 index 02c35eb..0000000 --- a/templates/webofthings/LICENSE.mustache +++ /dev/null @@ -1,2 +0,0 @@ -{{&licenseName}} -{{&licenseUrl}} diff --git a/templates/webofthings/README.md.mustache b/templates/webofthings/README.md.mustache deleted file mode 100644 index 7ee17fc..0000000 --- a/templates/webofthings/README.md.mustache +++ /dev/null @@ -1,36 +0,0 @@ -{{projectName}} -===================== - -Node-RED node for {{nodeName}} - -{{description}} - -Install -------- - -Run the following command in your Node-RED user directory - typically `~/.node-red` - - npm install {{projectName}} - -Interactions ------------- - -### Properties - -{{#properties}} -- {{label}}: {{description}} -{{/properties}} - -### Actions - -{{#actions}} -- {{label}}: {{description}} -{{/actions}} - - -### Events - -{{#events}} -- {{label}}: {{description}} -{{/events}} - diff --git a/templates/webofthings/node.html.mustache b/templates/webofthings/node.html.mustache deleted file mode 100644 index cd708f9..0000000 --- a/templates/webofthings/node.html.mustache +++ /dev/null @@ -1,152 +0,0 @@ - - - - - diff --git a/templates/webofthings/node.js.mustache b/templates/webofthings/node.js.mustache deleted file mode 100644 index 180c52c..0000000 --- a/templates/webofthings/node.js.mustache +++ /dev/null @@ -1,295 +0,0 @@ -module.exports = function (RED) { - "use strict"; - const request = require("request"); - - function formSelection(interaction, forms) { - if (interaction === "property-read") { - for (const f of forms) { - if (f.rel && f.rel === "readProperty") { - return f; - } - } - return forms[0]; - } else if (interaction === "property-write") { - for (const f of forms) { - if (f.rel && f.rel === "writeProperty") { - return f; - } - } - return forms[0]; - } else if (interaction === "property-observe" || interaction === "event") { - for (const f of forms) { - if (f.rel && f.rel === "observeProperty") { - if (f.href && f.href.match(/^htt/)) { - return f; - } - } - if (f.subProtocol && f.subProtocol === "LongPoll") { - return f; - } - } - return forms[0]; - } else if (interaction === "action") { - return forms[0]; - } else { - return forms[0]; - } - } - - function {{className}}Node(config) { - RED.nodes.createNode(this, config); - const node = this; - node.interactiontype = config.interactiontype; - node.propname = config.propname; - node.proptype = config.proptype; - node.actname = config.actname; - node.evname = config.evname; - console.log(JSON.stringify(node)); - const username = node.credentials.username; - const password = node.credentials.password; - const token = node.credentials.token; - const auth = {}; - // if username and password is set, use it. - if (username && password) { - auth.user = username; - auth.pass = password; - auth.sendImmediately = true; - } else if (token) { - auth.bearer = token - auth.sendImmediately = true; - } - - node.td = JSON.parse(`{{&tdstr}}`); - if (node.interactiontype === "property") { - if (node.proptype === "read") { - node.on("input", (msg) => { - const prop = node.td.properties[node.propname]; - const options = {}; - // select a suitable form - const form = formSelection("property-read", prop.forms); - const href = form.href; - if (href.match(/^https?:\/\//)) { - options.uri = href; - } else { - options.uri = node.td.base + href; - } - options.rejectUnauthorized = false; - if (auth.user || auth.bearer) { - options.auth = auth; - } - options.method = form["http:methodName"] ? form["http:methodName"] : "GET"; - console.log(JSON.stringify(options)); - request(options, (err, res, body) => { - if (err) { - msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; - msg.statusCode = err.code; - node.send(msg); - } else { - msg.statusCode = res.statusCode; - msg.headers = res.headers; - msg.responseUrl = res.request.uri.href; - if (prop.forms[0].mediaType && - prop.forms[0].mediaType === "application/json") { - msg.payload = JSON.parse(body); - } else { - msg.payload = body; - } - // TODO: validation of return value - node.send(msg); - } - }); - }); - } else if (node.proptype === "write") { - node.on("input", (msg) => { - const prop = node.td.properties[node.propname]; - const options = {}; - const form = formSelection("property-write", prop.forms); - const href = form.href; - if (href.match(/^https?:\/\//)) { - options.uri = href; - } else { - options.uri = node.td.base + href; - } - options.rejectUnauthorized = false; - if (form.mediaType && - form.mediaType === "application/json") { - options.json = true; - } else { - options.json = false; - } - options.body = msg.payload; - if (auth.user || auth.bearer) { - options.auth = auth; - } - options.method = form["http:methodName"] ? form["http:methodName"] : "PUT"; - console.log(JSON.stringify(options)); - request(options, (err, res, body) => { - if (err) { - msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; - msg.statusCode = err.code; - node.send(msg); - } else { - msg.statusCode = res.statusCode; - msg.headers = res.headers; - msg.responseUrl = res.request.uri.href; - msg.payload = body; - // TODO: validation of return value - node.send(msg); - } - }); - }); - } else if (node.proptype === "observe") { - // long polling only - // use input as trigger of request - let timerId; - node.on("input", (msg) => { - const prop = node.td.properties[node.propname]; - const options = {}; - const form = formSelection("property-observe", prop.forms); - const href = form.href; - if (href.match(/^https?:\/\//)) { - options.uri = href; - } else { - options.uri = node.td.base + href; - } - options.rejectUnauthorized = false; - options.timeout = 60000; - if (auth.user || auth.bearer) { - options.auth = auth; - } - options.method = form["http:methodName"] ? form["http:methodName"] : "GET"; - console.log(JSON.stringify(options)); - request(options, (err, res, body) => { - if (err) { - msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; - msg.statusCode = err.code; - node.status({fill:"yellow",shape:"dot",text:"Polling timeout"}); - node.send(null); - timerId = setTimeout(() => { - node.emit("input",{}); - }, 5000); - } else { - node.status({fill:"green",shape:"dot",text:"OK"}); - msg.statusCode = res.statusCode; - msg.headers = res.headers; - msg.responseUrl = res.request.uri.href; - if (form.mediaType && - form.mediaType === "application/json") { - msg.payload = JSON.parse(body); - } else { - msg.payload = body; - } - // TODO: validation of return value - node.send(msg); - timerId = setTimeout(() => { - node.emit("input",{}); - }, 5000); - } - }); - }); - node.on("close", () => { - if (timerId) { - clearTimeout(timerId); - } - }); - node.emit("input",{}); - } - } else if (node.interactiontype === "action") { - node.on("input", (msg) => { - const act = node.td.actions[node.actname]; - const options = {}; - const form = formSelection("action", act.forms); - const href = form.href; - if (href.match(/^https?:\/\//)) { - options.uri = href; - } else { - options.uri = node.td.base + href; - } - options.rejectUnauthorized = false; - options.json = true; - options.body = msg.payload; - if (auth.user || auth.bearer) { - options.auth = auth; - } - options.method = form["http:methodName"] ? form["http:methodName"] : "POST"; - console.log(JSON.stringify(options)); - request(options, (err, res, body) => { - if (err) { - msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; - msg.statusCode = err.code; - node.send(msg); - } else { - msg.statusCode = res.statusCode; - msg.headers = res.headers; - msg.responseUrl = res.request.uri.href; - msg.payload = body; - // TODO: validation of return value - node.send(msg); - } - }); - }); - } else if (node.interactiontype === "event") { - // long polling only - // use input as trigger of request - let timerId; - node.on("input", (msg) => { - const ev = node.td.events[node.evname]; - const options = {}; - const form = formSelection("event", ev.forms); - const href = form.href; - if (href.match(/^https?:\/\//)) { - options.uri = href; - } else { - options.uri = node.td.base + href; - } - options.rejectUnauthorized = false; - options.timeout = 100000; - if (auth.user || auth.bearer) { - options.auth = auth; - } - options.method = form["http:methodName"] ? form["http:methodName"] : "GET"; - console.log(JSON.stringify(options)); - request(options, (err, res, body) => { - if (err) { - msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; - msg.statusCode = err.code; - node.status({fill:"yellow",shape:"dot",text:"Polling timeout"}); - node.send(null); - timerId = setTimeout(() => { - node.emit("input",{}); - }, 5000); - } else { - node.status({fill:"green",shape:"dot",text:"OK"}); - msg.statusCode = res.statusCode; - msg.headers = res.headers; - msg.responseUrl = res.request.uri.href; - if (form.mediaType && - form.mediaType === "application/json") { - msg.payload = JSON.parse(body); - } else { - msg.payload = body; - } - // TODO: validation of return value - node.send(msg); - timerId = setTimeout(() => { - node.emit("input",{}); - }, 5000); - } - }); - }); - node.on("close", () => { - if (timerId) { - clearTimeout(timerId); - } - }); - node.emit("input", {}); - } - }; - RED.nodes.registerType("{{&nodeName}}", {{&className}}Node, { - credentials: { - token: {type:"password"}, - username: {type:"text"}, - password: {type:"password"} - } - }); -}; \ No newline at end of file diff --git a/templates/webofthings/package.json.mustache b/templates/webofthings/package.json.mustache deleted file mode 100644 index d724032..0000000 --- a/templates/webofthings/package.json.mustache +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "{{&projectName}}", - "version": "{{&projectVersion}}", - "description": "Node-RED node for {{&nodeName}}", - "main": "node.js", - "scripts": { - "test": "mocha \"test/**/*_spec.js\"" - }, - "node-red": { - "nodes": { - "{{&nodeName}}": "node.js" - } - }, - "keywords": [ - {{#keywords}} - "{{name}}"{{^last}}, {{/last}} - {{/keywords}} - ], - "dependencies": { - "request": "^2.87.0" - }, - "devDependencies": { - "node-red": "0.18.7", - "node-red-node-test-helper": "0.1.8" - }, - "license": "{{&licenseName}}" -} \ No newline at end of file From 1ce47cf76f0c84ba1ad80558e014e48c784734d0 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Tue, 25 Sep 2018 20:18:45 +0900 Subject: [PATCH 05/40] Set default category to Web of Things. --- lib/nodegen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nodegen.js b/lib/nodegen.js index 2c244b1..a2f41c6 100644 --- a/lib/nodegen.js +++ b/lib/nodegen.js @@ -548,7 +548,7 @@ function wottd2node(data, options) { data.projectName = data.module; data.projectVersion = data.version; data.keywords = extractKeywords(data.keywords); - data.category = data.category || 'function'; + data.category = data.category || 'Web of Things'; data.description = td.description; data.licenseName = 'Apache-2.0'; data.licenseUrl = ''; From 7d73c2efc54ccbf8653a887843c31c328b050fe3 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Tue, 25 Sep 2018 22:32:43 +0900 Subject: [PATCH 06/40] Fix event handling code --- templates/webofthings/node.js.mustache | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/templates/webofthings/node.js.mustache b/templates/webofthings/node.js.mustache index ad6df79..00f2dac 100644 --- a/templates/webofthings/node.js.mustache +++ b/templates/webofthings/node.js.mustache @@ -236,7 +236,8 @@ module.exports = function (RED) { } else if (node.interactiontype === "event") { // long polling only // use input as trigger of request - let timerId; + let timerId = null; + let reqObj = null; node.on("input", (msg) => { const ev = node.td.events[node.evname]; const options = {}; @@ -254,7 +255,7 @@ module.exports = function (RED) { } options.method = form["http:methodName"] ? form["http:methodName"] : "GET"; console.log(JSON.stringify(options)); - request(options, (err, res, body) => { + reqObj = request(options, (err, res, body) => { if (err) { msg.payload = `${err.toString()}: ${options.baseUrl}${options.uri}`; msg.statusCode = err.code; @@ -285,6 +286,10 @@ module.exports = function (RED) { node.on("close", () => { if (timerId) { clearTimeout(timerId); + timerId = null; + } + if (reqObj) { + reqObj.abort(); } }); node.emit("input", {}); From 0b9ebc5c17946d6acca996b051a3d53bf1977cda Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Fri, 28 Sep 2018 22:59:13 +0900 Subject: [PATCH 07/40] Fix bugs discovered in online plugfest --- templates/webofthings/node.html.mustache | 2 +- templates/webofthings/node.js.mustache | 26 +++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/templates/webofthings/node.html.mustache b/templates/webofthings/node.html.mustache index cd708f9..e3f087e 100644 --- a/templates/webofthings/node.html.mustache +++ b/templates/webofthings/node.html.mustache @@ -1,7 +1,7 @@ @@ -82,7 +204,7 @@ @@ -90,7 +212,7 @@ @@ -98,7 +220,7 @@ @@ -110,6 +232,11 @@ +
+ + +
- +
@@ -254,52 +263,76 @@ diff --git a/templates/webofthings/node.js.mustache b/templates/webofthings/node.js.mustache index 4d2dc5d..a037918 100644 --- a/templates/webofthings/node.js.mustache +++ b/templates/webofthings/node.js.mustache @@ -1,6 +1,6 @@ module.exports = function (RED) { "use strict"; - const request = require("request"); + const request = require('request'); const url = require('url'); const HttpsProxyAgent = require('https-proxy-agent'); const WebSocket = require('ws'); @@ -22,6 +22,129 @@ module.exports = function (RED) { return "application/json"; } + function coapMethodCodeToName(methodCode) { + switch (methodCode) { + case '1': + case '0.01': + case 'get': + case 'GET': + return 'get'; + break; + case '2': + case '0.02': + case 'post': + case 'POST': + return 'post'; + break; + case '3': + case '0.03': + case 'put': + case 'PUT': + return 'put'; + break; + case '4': + case '0.04': + case 'delete': + case 'DELETE': + return 'delete'; + break; + default: + return 'get'; + break; + } + } + + function bindingCoap(node, form, options={}) { // options.psk + const coap = require("node-coap-client").CoapClient; + node.trace("bindingCoap called"); + const msg = options.msg || {}; + const resource = extractTemplate(form.href, options.urivars); + let payload = null; + let method = null; + + if (options.interaction === "property-read") { + method = form.hasOwnProperty("cov:methodName") ? + coapMethodCodeToName(form['cov:methodName']) : 'get'; + } else if (options.interaction === "property-write") { + method = form.hasOwnProperty("cov:methodName") ? + coapMethodCodeToName(form['cov:methodName']) : 'put'; + payload = options.reqbody; + } else { // assume "action" + method = form.hasOwnProperty("cov:methodName") ? + coapMethodCodeToName(form['cov:methodName']) : 'post'; + payload = options.reqbody; + } + + node.trace(`CoAP request: resource=${resource}, method=${method}, payload=${payload}`); + coap.request(resource, method, payload) + .then(response => { // code, format, payload + node.trace(`CoAP response: code=${response.code.toString()}, format=${response.format}, payload=${response.payload}`); + if (response.format === 50) { // application/json; rfc7252 section 12.3, table 9 + try { + msg.payload = JSON.parse(response.payload); + } catch (e) { + msg.payload = response.payload; + } + } else { + msg.payload = response.payload; + } + if (options.outschema) { + const ajv = new Ajv({allErrors: true}); + if (!ajv.validate(options.outschema, msg.payload)) { + node.warn(`output schema validation error: ${ajv.errorsText()}`, msg); + } + } + node.send(msg); + }) + .catch(err => { + node.log(`Error: ${err.toString()}`); + msg.payload = `${err.toString()}: ${resource}`; + node.send(msg); + }); + } + + function bindingCoapObserve(node, form, options={}) { + const coap = require("node-coap-client").CoapClient; + node.status({fill:"yellow",shape:"dot",text:"CoAP try to observe ..."}); + const resource = extractTemplate(form.href,options.urivars); + const method = form.hasOwnProperty("cov:methodName") ? + coapMethodCodeToName(form['cov:methodName']) : 'get'; + const payload = options.reqbody; + const callback = response => { // code, format, payload + const msg = {}; + node.trace(`CoAP observe: code=${response.code.toString()}, format=${response.format}, payload=${response.payload}`); + if (response.format === 50) { // application/json; rfc7252 section 12.3, table 9 + try { + msg.payload = JSON.parse(response.payload); + } catch (e) { + msg.payload = response.payload; + } + } else { + msg.payload = response.payload; + } + if (options.outschema) { + const ajv = new Ajv({allErrors: true}); + if (!ajv.validate(options.outschema, msg.payload)) { + node.warn(`output schema validation error: ${ajv.errorsText()}`, msg); + } + } + node.send(msg); + }; + coap.observe(resource,method,callback,payload,options) + .then(() => { + node.status({fill:"green",shape:"dot",text:"CoAP Observing"}); + }) + .catch(err => { + node.status({fill:"red",shape:"dot",text:`CoAP Error: ${err}`}); + node.log(`Error: ${err.toString()}`); + }); + node.on('close', () => { + node.trace('Close node'); + coap.stopObserving(resource); + node.status({}); + }); + } + function bindingWebSocket(node, form, options={}) { let ws; let reconnectTimeout; @@ -74,7 +197,7 @@ module.exports = function (RED) { if (options.outschema) { const ajv = new Ajv({allErrors: true}); if (!ajv.validate(options.outschema, msg.payload)) { - node.warn(`ouput schema validation error: ${ajv.errorsText()}`, msg); + node.warn(`output schema validation error: ${ajv.errorsText()}`, msg); } } node.send(msg) @@ -106,7 +229,7 @@ module.exports = function (RED) { if (options && options.auth && (options.auth.user || options.auth.bearer)) { reqoptions.auth = options.auth; } - reqoptions.method = form.hasOwnProperty("http:methodName") ? form["http:methodName"] : "GET"; + reqoptions.method = form.hasOwnProperty("htv:methodName") ? form["htv:methodName"] : "GET"; node.trace(`LongPoll Request options: ${JSON.stringify(reqoptions)}`); needReconnect = true; reqObj = request(reqoptions, (err, res, body) => { @@ -138,7 +261,7 @@ module.exports = function (RED) { if (options.outschema) { const ajv = new Ajv(); if (!ajv.validate(options.outschema, msg.payload)) { - node.warn(`ouput schema validation error: ${ajv.errorsText()}`, msg); + node.warn(`output schema validation error: ${ajv.errorsText()}`, msg); } } node.send(msg); @@ -170,9 +293,9 @@ module.exports = function (RED) { reqoptions.auth = options.auth; } if (options.interaction === "property-read") { - reqoptions.method = form.hasOwnProperty("http:methodName") ? form["http:methodName"] : "GET"; + reqoptions.method = form.hasOwnProperty("htv:methodName") ? form["htv:methodName"] : "GET"; } else if (options.interaction === "property-write") { - reqoptions.method = form.hasOwnProperty("http:methodName") ? form["http:methodName"] : "PUT"; + reqoptions.method = form.hasOwnProperty("htv:methodName") ? form["htv:methodName"] : "PUT"; switch (reqoptions.method) { case "GET": break; @@ -184,7 +307,7 @@ module.exports = function (RED) { break; } } else { // assume "action" - reqoptions.method = form.hasOwnProperty("http:methodName") ? form["http:methodName"] : "POST"; + reqoptions.method = form.hasOwnProperty("htv:methodName") ? form["htv:methodName"] : "POST"; switch (reqoptions.method) { case "GET": break; @@ -279,7 +402,11 @@ module.exports = function (RED) { node.warn(`input schema validation error: ${ajv.errorsText()}`, msg); } } - bindingHttp(node, form, {interaction:"property-read", auth, msg, urivars, outschema: prop}); + if (form.href.match(/^https?:/)) { + bindingHttp(node, form, {interaction:"property-read", auth, msg, urivars, outschema: prop}); + } else if (form.href.match(/^coaps?:/)) { + bindingCoap(node, form, {interaction:"property-read", auth, msg, urivars, outschema: prop}); + } }); } else if (node.proptype === "write") { node.on("input", (msg) => { @@ -291,7 +418,11 @@ module.exports = function (RED) { node.warn(`input schema validation error: ${ajv.errorsText()}`, msg); } // URI template is not supported, because 'write' doesn't use GET method. - bindingHttp(node, form, {interaction: "property-write", auth, msg, reqbody: msg.payload}); + if (form.href.match(/^https?:/)) { + bindingHttp(node, form, {interaction: "property-write", auth, msg, reqbody: msg.payload}); + } else if (form.href.match(/^coaps?:/)) { + bindingCoap(node, form, {interaction: "property-write", auth, msg, reqbody: msg.payload}); + } }); } else if (node.proptype === "observe") { const prop = normTd.properties[node.propname]; @@ -306,6 +437,8 @@ module.exports = function (RED) { } if (form.href.match(/^wss?:/)) { // websocket bindingWebSocket(node, form, {auth, urivars, outschema: prop}); + } else if (form.href.match(/^coaps?:/)) { // CoAP observe + bindingCoapObserve(node, form, {auth, urivars, outschema: prop}); } else { // long polling bindingLongPoll(node, form, {auth, urivars, outschema: prop}); } @@ -328,7 +461,11 @@ module.exports = function (RED) { node.warn(`input schema validation error: ${ajv.errorsText()}`, msg); } } - bindingHttp(node, form, {interaction: "action", auth, msg, urivars, reqbody:msg.payload}); + if (form.href.match(/^https?:/)) { + bindingHttp(node, form, {interaction: "action", auth, msg, urivars, reqbody:msg.payload}); + } else if (form.href.match(/^coaps?/)) { + bindingCoap(node, form, {interaction: "action", auth, msg, urivars, reqbody:msg.payload}); + } }); } else if (node.interactiontype === "event") { const ev = normTd.events[node.evname]; @@ -343,6 +480,8 @@ module.exports = function (RED) { } if (form.href.match(/^wss?:/)) { // websocket bindingWebSocket(node, form, {auth, urivars}); + } else if (form.href.match(/^coaps?:/)) { + bindingCoapObserve(node, form, {auth, urivars}); } else { // long polling bindingLongPoll(node, form, {auth, urivars}); } diff --git a/templates/webofthings/package.json.mustache b/templates/webofthings/package.json.mustache index 5efb37f..1fd53f5 100644 --- a/templates/webofthings/package.json.mustache +++ b/templates/webofthings/package.json.mustache @@ -21,7 +21,8 @@ "request": "^2.88.0", "ws": "^6.1.2", "url-template": "^2.0.8", - "ajv": "^6.7.0" + "ajv": "^6.7.0", + "node-coap-client": "^1.0.2" }, "devDependencies": { "node-red": "^0.19.5", From 9ba2847027224121c37298ac6f9b3aed2416b5c6 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Sun, 19 May 2019 19:23:52 +0900 Subject: [PATCH 23/40] remove debug message --- lib/wotutils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/wotutils.js b/lib/wotutils.js index 58a8b33..8c571e5 100644 --- a/lib/wotutils.js +++ b/lib/wotutils.js @@ -175,7 +175,6 @@ function normalizeTd(td) { readOnly: !isOpInForms("writeproperty", convforms), observable: false }; - console.log(JSON.stringify(td.properties['__ALLPROPERTIES'],null,2)); } return td; From d90fcb6c69eefd82a8b9928b20e3423ffe2f600f Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Tue, 21 May 2019 21:41:15 +0900 Subject: [PATCH 24/40] remove use of Array.prototype.flat() for better compatibility --- lib/wotutils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wotutils.js b/lib/wotutils.js index 8c571e5..07224f6 100644 --- a/lib/wotutils.js +++ b/lib/wotutils.js @@ -35,7 +35,7 @@ function setdefault(obj, propname, dflt) { function isOpInForms(op, forms) { return forms && forms .map(e=>(e.op && typeof e.op === 'string') ? [e.op] : e.op) - .flat() + .reduce((a,v)=>a.concat(v),[]) .some(e=>e === op); } @@ -160,7 +160,7 @@ function normalizeTd(td) { res = 'readproperty'; break; case 'writeallproperties': - res = 'writepropety'; + res = 'writeproperty'; } return res; }); From 73a5e1dfeb7b0df6207beff666465758c400acb5 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Mon, 27 May 2019 12:07:38 +0900 Subject: [PATCH 25/40] fix default value handling of PropertyAffordance --- lib/wotutils.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/wotutils.js b/lib/wotutils.js index 07224f6..8de5ff0 100644 --- a/lib/wotutils.js +++ b/lib/wotutils.js @@ -102,7 +102,12 @@ function normalizeTd(td) { // Set default values in properties for (const p in td.properties) { - const pdef = td.properties[p]; + const pdef = td.properties[p]; + if (pdef.forms) { + pdef.forms = pdef.forms.map((f) => formconv(pdef, f, "PropertyAffordance")); + // no filtering based on protocol + } + // if there is forms which have readproperty in op, writeOnly is false, otherwise true pdef.writeOnly = setdefault(pdef, "writeOnly", !isOpInForms("readproperty", pdef.forms)); // if there is forms which have writeproperty in op, readOnly is false, otherwise true @@ -111,10 +116,6 @@ function normalizeTd(td) { pdef.observable = setdefault(pdef, "observable", isOpInForms("observeproperty", pdef.forms)); // in any cases, if it explicitly stated by writeOnly/readOnly/observable, use it. - if (pdef.forms) { - pdef.forms = pdef.forms.map((f) => formconv(pdef, f, "PropertyAffordance")); - // no filtering based on protocol - } } // Set default values in actions From 0e52f93605eedd31773802ddbd4f8aaccbf0a5db Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Sat, 1 Jun 2019 11:32:08 +0200 Subject: [PATCH 26/40] Normalize 'security' as an array of string --- lib/wotutils.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/wotutils.js b/lib/wotutils.js index 8de5ff0..a2d2812 100644 --- a/lib/wotutils.js +++ b/lib/wotutils.js @@ -47,6 +47,9 @@ function normalizeTd(td) { if (f.hasOwnProperty("href")) { f.href = decodeURIComponent(url.resolve(baseUrl, f.href)); } + if (f.hasOwnProperty("security") && typeof f.security === 'string') { + f.security = [f.security]; + } if (!f.hasOwnProperty("security")) { if (intr.hasOwnProperty("security")) { f.security = intr.security; @@ -70,6 +73,11 @@ function normalizeTd(td) { return f; } + // normalize 'security' as Array of String. + if (td.hasOwnProperty("security") && typeof td.security === "string") { + td.security = [td.security]; + } + // Set default values in security definition for (const sd in td.securityDefinitions) { const sdef = td.securityDefinitions[sd]; From 4c56ce3c2c5ed73dcc20588f7d1996f0580613b3 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Sat, 1 Jun 2019 18:28:47 +0200 Subject: [PATCH 27/40] support binary data --- templates/webofthings/node.js.mustache | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/templates/webofthings/node.js.mustache b/templates/webofthings/node.js.mustache index a037918..3948cb9 100644 --- a/templates/webofthings/node.js.mustache +++ b/templates/webofthings/node.js.mustache @@ -22,6 +22,20 @@ module.exports = function (RED) { return "application/json"; } + function isBinaryType(contentType) { + let result; + switch (contentType) { + case "image/jpeg": + case "application/octet-stream": + result = true; + break; + default: + result = false; + break; + } + return result; + } + function coapMethodCodeToName(methodCode) { switch (methodCode) { case '1': @@ -230,6 +244,9 @@ module.exports = function (RED) { reqoptions.auth = options.auth; } reqoptions.method = form.hasOwnProperty("htv:methodName") ? form["htv:methodName"] : "GET"; + if (isBinaryType(form.contentType)) { + reqoptions.encoding = null; + } node.trace(`LongPoll Request options: ${JSON.stringify(reqoptions)}`); needReconnect = true; reqObj = request(reqoptions, (err, res, body) => { @@ -316,9 +333,15 @@ module.exports = function (RED) { reqoptions.json = form.contentType === "application/json"; reqoptions.body = options.reqbody; reqoptions.headers = {'Content-Type': form.contentType}; + if (form.hasOwnProperty("response") && isBinaryType(form.response.contentType)) { + reqoptions.encoding = null; + } break; } } + if (isBinaryType(form.contentType)) { + reqoptions.encoding = null; + } node.trace(`HTTP request options: ${JSON.stringify(reqoptions)}`); request(reqoptions, (err, res, body) => { if (err) { From 95e8acf850a55ddf4dae43931e8edfe30ac7da41 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Sun, 2 Jun 2019 14:42:05 +0200 Subject: [PATCH 28/40] support HTTP and language negotiation for retrieve WoT Thing Description (use --wottd and --lang) --- bin/node-red-nodegen.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/bin/node-red-nodegen.js b/bin/node-red-nodegen.js index 3e7fa65..e1cdd7e 100755 --- a/bin/node-red-nodegen.js +++ b/bin/node-red-nodegen.js @@ -56,6 +56,8 @@ function help() { ' [--color ]' + ' [--tgz]' + ' [--help]' + + ' [--wottd]' + + ' [--lang ]' + ' [-v]\n' + '\n' + 'Description:'.bold + '\n' + @@ -79,6 +81,8 @@ function help() { ' --color : color for node appearance (format: color hexadecimal numbers like "A6BBCF")\n' + ' --tgz : Save node as tgz file\n' + ' --help : Show help\n' + + ' --wottd : Get Thing Description via HTTP\n' + + ' --lang : Language negotiation\n' + ' -v : Show node generator version\n'; console.log(helpText); } @@ -95,7 +99,7 @@ if (argv.help || argv.h) { } else { var sourcePath = argv._[0]; if (sourcePath) { - if (sourcePath.startsWith('http://') || sourcePath.startsWith('https://')) { + if (!argv.wottd && (sourcePath.startsWith('http://') || sourcePath.startsWith('https://'))) { request(sourcePath, function (error, response, body) { if (!error) { data.src = JSON.parse(body); @@ -108,6 +112,27 @@ if (argv.help || argv.h) { console.error(error); } }); + } else if (argv.wottd && (sourcePath.startsWith('http://') || sourcePath.startsWith('https://'))) { + const req = { + url: sourcePath, + } + if (argv.lang) { + req.headers = { + 'Accept-Language': argv.lang + } + } + request(req, function (error, response, body) { + if (!error) { + data.src = JSON.parse(body); + nodegen.wottd2node(data, options).then(function (result) { + console.log('Success: ' + result); + }).catch(function (error) { + console.log('Error: ' + error); + }); + } else { + console.error(error); + } + }); } else if (sourcePath.endsWith('.json')) { data.src = JSON.parse(fs.readFileSync(sourcePath)); nodegen.swagger2node(data, options).then(function (result) { From 3dd7a0eae60da90af86418777a1f36ad0f1c05a4 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Sun, 2 Jun 2019 14:47:21 +0200 Subject: [PATCH 29/40] update README.md (HTTP support for retrieve TD) --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 7e3a48d..8a8c06d 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ You may need to run this with `sudo`, or from within an Administrator command sh --category : Node category (default: "function") --tgz : Save node as tgz file --help : Show help + --wottd : Get Thing Description via HTTP + --lang : Language negotiation ### Example 1. Create original node from Swagger definition @@ -72,6 +74,16 @@ You may need to run this with `sudo`, or from within an Administrator command sh -> You can use Example Thing node on Node-RED flow editor. +### Example 4. Create original node from Thing Description via HTTP + +- node-red-nodegen http://example.com/td.jsonld --wottd --lang "en-US,en;q=0.5" +- cd node-red-contrib-example-thing +- sudo npm link +- cd ~/.node-red +- npm link node-red-contrib-example-thing +- node-red + +-> You can use Example Thing node on Node-RED flow editor. ## Documentation - [Use cases](docs/index.md#use-cases) ([Japanese](docs/index_ja.md#use-cases)) From 55895a67fc722a5cd9ce234e3682af5490c282cf Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Sun, 2 Jun 2019 14:58:07 +0200 Subject: [PATCH 30/40] when use --wottd, regard .json as TD --- README.md | 4 ++-- bin/node-red-nodegen.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8a8c06d..97a5cd8 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ You may need to run this with `sudo`, or from within an Administrator command sh --category : Node category (default: "function") --tgz : Save node as tgz file --help : Show help - --wottd : Get Thing Description via HTTP - --lang : Language negotiation + --wottd : explicitly instruct that source file/URL points a Thing Description + --lang : Language negotiation information when retrieve a Thing Description ### Example 1. Create original node from Swagger definition diff --git a/bin/node-red-nodegen.js b/bin/node-red-nodegen.js index e1cdd7e..13c41dd 100755 --- a/bin/node-red-nodegen.js +++ b/bin/node-red-nodegen.js @@ -81,8 +81,8 @@ function help() { ' --color : color for node appearance (format: color hexadecimal numbers like "A6BBCF")\n' + ' --tgz : Save node as tgz file\n' + ' --help : Show help\n' + - ' --wottd : Get Thing Description via HTTP\n' + - ' --lang : Language negotiation\n' + + ' --wottd : explicitly instruct source file/URL points a Thing Description\n' + + ' --lang : Language negotiation information when retrieve a Thing Description\n' + ' -v : Show node generator version\n'; console.log(helpText); } @@ -133,7 +133,7 @@ if (argv.help || argv.h) { console.error(error); } }); - } else if (sourcePath.endsWith('.json')) { + } else if (sourcePath.endsWith('.json') && !argv.wottd) { data.src = JSON.parse(fs.readFileSync(sourcePath)); nodegen.swagger2node(data, options).then(function (result) { console.log('Success: ' + result); @@ -154,7 +154,7 @@ if (argv.help || argv.h) { }).catch(function (error) { console.log('Error: ' + error); }); - } else if (sourcePath.endsWith('.jsonld')) { + } else if (sourcePath.endsWith('.jsonld') || argv.wottd) { data.src = JSON.parse(fs.readFileSync(sourcePath)); nodegen.wottd2node(data, options).then(function (result) { console.log('Success: ' + result); From af0669765f4f9b6eb7af3b2bfae0a07bac4e6832 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Sun, 2 Jun 2019 15:08:13 +0200 Subject: [PATCH 31/40] editorial fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97a5cd8..b440317 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ You may need to run this with `sudo`, or from within an Administrator command sh Supported source: - Function node (js file in library, "~/.node-red/lib/function/") - Swagger definition - - Thing Description of W3C Web of Things (jsonld file in library, or URL that points jsonld file) + - Thing Description of W3C Web of Things (jsonld file or URL that points jsonld file) Options: -o : Destination path to save generated node (default: current directory) From dbf58f2f826b5a93dbab9f0b4884ab97ccac423d Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Thu, 6 Jun 2019 17:17:21 +0200 Subject: [PATCH 32/40] skip BOM when exist --- bin/node-red-nodegen.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bin/node-red-nodegen.js b/bin/node-red-nodegen.js index 13c41dd..1444807 100755 --- a/bin/node-red-nodegen.js +++ b/bin/node-red-nodegen.js @@ -92,6 +92,16 @@ function version() { console.log(packageJson.version); } +function skipBom(body) { + if (body[0]===0xEF && + body[1]===0xBB && + body[2]===0xBF) { + return body.slice(3); + } else { + return body; + } +} + if (argv.help || argv.h) { help(); } else if (argv.v) { @@ -123,7 +133,7 @@ if (argv.help || argv.h) { } request(req, function (error, response, body) { if (!error) { - data.src = JSON.parse(body); + data.src = JSON.parse(skipBom(body)); nodegen.wottd2node(data, options).then(function (result) { console.log('Success: ' + result); }).catch(function (error) { @@ -155,7 +165,7 @@ if (argv.help || argv.h) { console.log('Error: ' + error); }); } else if (sourcePath.endsWith('.jsonld') || argv.wottd) { - data.src = JSON.parse(fs.readFileSync(sourcePath)); + data.src = JSON.parse(skipBom(fs.readFileSync(sourcePath))); nodegen.wottd2node(data, options).then(function (result) { console.log('Success: ' + result); }).catch(function (error) { From 0bcc6290684425111f6b16ba38b50ce9ea5b9b75 Mon Sep 17 00:00:00 2001 From: Kunihiko Toumura Date: Mon, 10 Jun 2019 21:00:02 +0900 Subject: [PATCH 33/40] dir='auto' for heuristic directionality detection --- templates/webofthings/node.html.mustache | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/templates/webofthings/node.html.mustache b/templates/webofthings/node.html.mustache index 2662f77..db52ada 100644 --- a/templates/webofthings/node.html.mustache +++ b/templates/webofthings/node.html.mustache @@ -212,7 +212,7 @@
- {{#properties}} {{/properties}} @@ -220,7 +220,7 @@
- {{#actions}} {{/actions}} @@ -228,7 +228,7 @@
- {{#events}} {{/events}} @@ -264,7 +264,7 @@