diff --git a/.github/workflows/validate-deps.js b/.github/workflows/validate-deps.js new file mode 100644 index 0000000..8c04e7a --- /dev/null +++ b/.github/workflows/validate-deps.js @@ -0,0 +1,18 @@ +import fs from 'fs' +import path from 'path' +import { checkDependencies } from '../../repo-convention.js' + +for (let i = 1; i < process.argv.length; i++) { + const file = path.normalize((path.join(process.cwd(), process.argv[i]))) + if (file.startsWith(path.join(process.cwd(), 'dtmi/'))) { + console.log('\nchecking: ' + file) + const dtdlJson = JSON.parse(fs.readFileSync(file, 'utf-8')) + const id = dtdlJson['@id'] + console.log('Scanning dependencies for: ' + id) + if (!checkDependencies(id)) { + process.exit(1) + } + } else { + console.debug('Skipping file: ' + file) + } +} diff --git a/.github/workflows/validate-files.js b/.github/workflows/validate-files.js new file mode 100644 index 0000000..af126a3 --- /dev/null +++ b/.github/workflows/validate-files.js @@ -0,0 +1,13 @@ +import path from 'path' +import { checkDtmiPathFromFile } from '../../repo-convention.js' + +for (let i = 1; i < process.argv.length; i++) { + const file = path.normalize(process.argv[i]) + const fullFilePath = path.normalize((path.join(process.cwd(), process.argv[i]))) + if (fullFilePath.startsWith(path.join(process.cwd(), 'dtmi/'))) { + console.log('\nchecking: ' + file) + checkDtmiPathFromFile(file) + } else { + console.debug('Skipping file: ' + file) + } +} diff --git a/.github/workflows/validate-files.yml b/.github/workflows/validate-files.yml new file mode 100644 index 0000000..33c481b --- /dev/null +++ b/.github/workflows/validate-files.yml @@ -0,0 +1,37 @@ +name: validate-files + +on: + push: + branches: [rido/*] + + pull_request: + branches: [main] + paths: + - 'dtmi/**' + +jobs: + validate-files: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - id: files + uses: jitterbit/get-changed-files@v1 + + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: npm install + run: npm install + + - name: check DTMI path convention + run: node .github/workflows/validate-files.js ${{steps.files.outputs.added_modified}} + + - name: check global IDs + run: node .github/workflows/validate-ids.js ${{steps.files.outputs.added_modified}} + + - name: check dependencies + run: node .github/workflows/validate-deps.js ${{steps.files.outputs.added_modified}} diff --git a/.github/workflows/validate-ids.js b/.github/workflows/validate-ids.js new file mode 100644 index 0000000..fd26417 --- /dev/null +++ b/.github/workflows/validate-ids.js @@ -0,0 +1,17 @@ +import fs from 'fs' +import path from 'path' + +import { checkIds } from '../../repo-convention.js' + +for (let i = 2; i < process.argv.length; i++) { + const file = path.normalize((path.join(process.cwd(), process.argv[i]))) + if (file.startsWith(path.join(process.cwd(), 'dtmi/'))) { + console.log('\nchecking: ' + file) + if (!checkIds(JSON.parse(fs.readFileSync(file, 'utf-8')))) { + console.error(`ERROR validating ids on file ${file}. Aborting.`) + process.exit(1) + } + } else { + console.debug('Skipping file: ' + file) + } +} diff --git a/.gitignore b/.gitignore index 48e4eaa..f078218 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -.npmrc \ No newline at end of file +.npmrc +out/ \ No newline at end of file diff --git a/.npmignore b/.npmignore index 85aabbd..690b1fd 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,4 @@ .github/ dtmi/ -*.test.js \ No newline at end of file +out/ +test/ diff --git a/README.md b/README.md index 9189231..8de6f54 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,21 @@ To use with `Node.js 12.x` add the `--experimental-modules` flag. (Tested with ` npm i @ridomin/repo-scripts ``` -```js -import { dtmi2path } from '@ridomin/repo-scripts/dtmi2path.js' -const dtmi = 'dtmi:com:example:Thermostat;1' -console.log(dtmi, '->', dtmi2path(dtmi)) -``` \ No newline at end of file +```ts +// main.ts +import * as rc from '@ridomin/repo-scripts' +console.log(rc.isDtmi('aaa')) +console.log(rc.dtmiToPath('dtmi:com:ee:aa;1')) +``` + +Build with + +```bash +tsc -t es6 --outDir ./out --moduleResolution node .\main.ts +``` + +Run with + +```bash +node ./out/main.js +``` diff --git a/dtmi/azure/devicemanagement/deviceinformation-1.json b/dtmi/azure/devicemanagement/deviceinformation-1.json new file mode 100644 index 0000000..8a37e6d --- /dev/null +++ b/dtmi/azure/devicemanagement/deviceinformation-1.json @@ -0,0 +1,64 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:azure:DeviceManagement:DeviceInformation;1", + "@type": "Interface", + "displayName": "Device Information", + "contents": [ + { + "@type": "Property", + "name": "manufacturer", + "displayName": "Manufacturer", + "schema": "string", + "description": "Company name of the device manufacturer. This could be the same as the name of the original equipment manufacturer (OEM). Ex. Contoso." + }, + { + "@type": "Property", + "name": "model", + "displayName": "Device model", + "schema": "string", + "description": "Device model name or ID. Ex. Surface Book 2." + }, + { + "@type": "Property", + "name": "swVersion", + "displayName": "Software version", + "schema": "string", + "description": "Version of the software on your device. This could be the version of your firmware. Ex. 1.3.45" + }, + { + "@type": "Property", + "name": "osName", + "displayName": "Operating system name", + "schema": "string", + "description": "Name of the operating system on the device. Ex. Windows 10 IoT Core." + }, + { + "@type": "Property", + "name": "processorArchitecture", + "displayName": "Processor architecture", + "schema": "string", + "description": "Architecture of the processor on the device. Ex. x64 or ARM." + }, + { + "@type": "Property", + "name": "processorManufacturer", + "displayName": "Processor manufacturer", + "schema": "string", + "description": "Name of the manufacturer of the processor on the device. Ex. Intel." + }, + { + "@type": "Property", + "name": "totalStorage", + "displayName": "Total storage", + "schema": "double", + "description": "Total available storage on the device in kilobytes. Ex. 2048000 kilobytes." + }, + { + "@type": "Property", + "name": "totalMemory", + "displayName": "Total memory", + "schema": "double", + "description": "Total available memory on the device in kilobytes. Ex. 256000 kilobytes." + } + ] +} \ No newline at end of file diff --git a/dtmi/test/badpath.json b/dtmi/test/badpath.json index d5b26bb..96eafae 100644 --- a/dtmi/test/badpath.json +++ b/dtmi/test/badpath.json @@ -2,7 +2,7 @@ "@context": "dtmi:dtdl:context;2", "@id": "dtmi:test:onedep;1", "@type": "Interface", - "displayName": "onedep", + "displayName": "onedep1", "extends": ["dtmi:test:base;1"], "contents": [ { diff --git a/dtmi/test/onedep-1.json b/dtmi/test/onedep-1.json index c468f15..2a37362 100644 --- a/dtmi/test/onedep-1.json +++ b/dtmi/test/onedep-1.json @@ -2,7 +2,7 @@ "@context": "dtmi:dtdl:context;2", "@id": "dtmi:test:onedep;1", "@type": "Interface", - "displayName": "onedep", + "displayName": "onedep1", "extends": "dtmi:test:base;1", "contents": [] } diff --git a/dtmi/test/twodeps-1.json b/dtmi/test/twodeps-1.json index c2484fa..638fdbd 100644 --- a/dtmi/test/twodeps-1.json +++ b/dtmi/test/twodeps-1.json @@ -2,7 +2,7 @@ "@context": "dtmi:dtdl:context;2", "@id": "dtmi:test:twodeps;1", "@type": "Interface", - "displayName": "onedep", + "displayName": "onedep1", "extends": ["dtmi:test:base;1"], "contents": [ { diff --git a/dtmi/test/uniqueids-1.json b/dtmi/test/uniqueids-1.json index 667dbfd..39ebdba 100644 --- a/dtmi/test/uniqueids-1.json +++ b/dtmi/test/uniqueids-1.json @@ -2,7 +2,7 @@ "@context": "dtmi:dtdl:context;2", "@id": "dtmi:test:uniqueids;1", "@type": "Interface", - "displayName": "onedep", + "displayName": "onedep1", "contents": [ { "@id": "dtmi:test:prop;1", diff --git a/dtmi2path.js b/dtmi2path.js deleted file mode 100644 index 163af98..0000000 --- a/dtmi2path.js +++ /dev/null @@ -1,14 +0,0 @@ -export /** - * @description Converts DTMI to dtmi/com/example/device-1.json path - * @param {string} dtmi - * @returns {string)} - */ -const dtmi2path = dtmi => { - if (RegExp('^dtmi:[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?(?::[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?)*;[1-9][0-9]{0,8}$').test(dtmi)) { - const idAndVersion = dtmi.toLowerCase().split(';') - const ids = idAndVersion[0].split(':') - const fileName = `${ids.pop()}-${idAndVersion[1]}.json` - const modelFolder = ids.join('/') - return `${modelFolder}/${fileName}` - } else return 'NOT-VALID-DTMI' -} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..409e0e4 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,42 @@ +declare module '@ridomin/repo-scripts' { + /** + * @description Validates DTMI with RegEx from https://github.com/Azure/digital-twin-model-identifier#validation-regular-expressions + * @param {string} dtmi + */ + function isDtmi(dtmi: string) : boolean; + + /** + * @description Converts DTMI to /dtmi/com/example/device-1.json path. + * @param {string} dtmi + * @returns {string} + */ + function dtmiToPath(dtmi: string) : string; + + /** + * @description Returns external IDs in `extend` and `component` elements + * @param {{ extends: any[]; contents: any[]; }} rootJson + * @returns {Array} + */ + function getDependencies(dtdlJson: any): Array; + + /** + * @description Checks all dependencies are available + * @param {Array} deps + * @returns {boolean} + */ + function checkDependencies(dtmi: string): boolean; + + /** + * @description Validates all internal IDs follow the namepspace set by the root id + * @param {any} dtdlJson + * @returns {boolean} + */ + function checkIds(dtdlJson: any): boolean; + + /** + * @description Copy DTDL file to the /dtmi/com/model-1.json folder struct + * @param file + * @returns The root DTMI of the file, if successfull + */ + function addModel(file: string): string; +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..42d0615 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +export { isDtmi, dtmiToPath, getDependencies, checkIds, checkDtmiPathFromFile } from './repo-convention.js' diff --git a/package-lock.json b/package-lock.json index a90ab44..31f36c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "@iotmodels/repo-scripts", - "version": "1.0.0", + "name": "@ridomin/repo-scripts", + "version": "0.0.13", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -712,6 +712,16 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "26.0.15", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz", + "integrity": "sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, "@types/node": { "version": "14.11.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz", @@ -1728,6 +1738,15 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -3991,13 +4010,9 @@ } }, "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, "ms": { "version": "2.1.2", @@ -4042,6 +4057,11 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6007,6 +6027,17 @@ "dev": true, "requires": { "mkdirp": "^0.5.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } } }, "write-file-atomic": { diff --git a/package.json b/package.json index 48b1e3e..8560809 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "@ridomin/repo-scripts", - "version": "0.0.5", + "version": "0.0.14", "description": "", - "main": "dtmi2path.js", + "main": "index.js", "type": "module", + "types": "index.d.ts", "scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "prepare": "standard && npm test" @@ -20,15 +21,23 @@ }, "homepage": "https://github.com/iotmodels/repo-scripts/#readme", "devDependencies": { + "@types/jest": "^26.0.15", "jest": "^26.4.2", "standard": "^14.3.4" }, "dependencies": { - "jsonata": "^1.8.3" + "jsonata": "^1.8.3", + "mkdirp": "^1.0.4", + "node-fetch": "^2.6.1" }, - "standard" : { - "globals" : [ - "test", "expect" + "standard": { + "globals": [ + "test", + "expect" + ], + "ignore": [ + "out/*", + "*.d.ts" ] } } diff --git a/repo-convention.js b/repo-convention.js index 8d8aeea..f058844 100644 --- a/repo-convention.js +++ b/repo-convention.js @@ -1,25 +1,41 @@ -import fs from 'fs' -import path from 'path' +import { readFileSync, existsSync } from 'fs' +import { join, resolve, normalize } from 'path' import jsonata from 'jsonata' -import { dtmi2path } from './dtmi2path.js' -export { dtmi2path } from './dtmi2path.js' +import fetch from 'node-fetch' -export /** - * @description Returns external IDs in extend and component schemas +export/** + * @description Validates DTMI with RegEx from https://github.com/Azure/digital-twin-model-identifier#validation-regular-expressions + * @param {string} dtmi + */ +const isDtmi = dtmi => RegExp('^dtmi:[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?(?::[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?)*;[1-9][0-9]{0,8}$').test(dtmi) + +export/** + * @description Converts DTMI to /dtmi/com/example/device-1.json path. + * @param {string} dtmi + * @returns {string} + */ +const dtmiToPath = dtmi => isDtmi(dtmi) ? `/${dtmi.toLowerCase().replace(/:/g, '/').replace(';', '-')}.json` : null + +export/** + * @description Returns external IDs in `extend` and `component` elements * @param {{ extends: any[]; contents: any[]; }} rootJson * @returns {Array} */ -const getDependencies = dtdlJson => { - const deps = [] - if (dtdlJson.extends) { - if (Array.isArray(dtdlJson.extends)) { - dtdlJson.extends.forEach(e => deps.push(e)) +const getDependencies = rootJson => { + let deps = [] + if (Array.isArray(rootJson)) { + deps = rootJson.map(d => d['@id']) + return deps + } + if (rootJson.extends) { + if (Array.isArray(rootJson.extends)) { + rootJson.extends.forEach(e => deps.push(e)) } else { - deps.push(dtdlJson.extends) + deps.push(rootJson.extends) } } - if (dtdlJson.contents) { - const comps = dtdlJson.contents.filter(c => c['@type'] === 'Component') + if (rootJson.contents) { + const comps = rootJson.contents.filter(c => c['@type'] === 'Component') comps.forEach(c => { if (typeof c.schema !== 'object') { if (deps.indexOf(c.schema) === -1) { @@ -31,6 +47,34 @@ const getDependencies = dtdlJson => { return deps } +export/** + * @description Checks all dependencies are available + * @param {Array} deps + * @returns {boolean} + */ +const checkDependencies = dtmi => { + let result = true + const fileName = join(__dirname, dtmiToPath(dtmi) || '') + console.log(`Validating dependencies for ${dtmi} from ${fileName}`) + const dtdlJson = JSON.parse(readFileSync(fileName, 'utf-8')) + const deps = getDependencies(dtdlJson) + deps.forEach(d => { + const fileName = join(__dirname, dtmiToPath(d) || '') + if (existsSync(fileName)) { + console.log(`Dependency ${d} found`) + const model = JSON.parse(readFileSync(fileName, 'utf-8')) + if (model['@id'] !== d) { + console.error(`ERROR: LowerCase issue with dependent id ${d}. Was ${model['@id']}. Aborting`) + result = result && true + } + } else { + console.error(`ERROR: Dependency ${d} NOT found. Aborting`) + result = false + } + }) + return result +} + export/** * @description Validates all internal IDs follow the namepspace set by the root id * @param {any} dtdlJson @@ -41,33 +85,38 @@ const checkIds = dtdlJson => { console.log(`checkIds: validating root ${rootId}`) const ids = jsonata('**."@id"').evaluate(dtdlJson) if (Array.isArray(ids)) { - for (const id in ids) { + for (let i = 0; i < ids.length; i++) { + const id = ids[i] console.log('found: ' + id) + if (!isDtmi(id)) { + console.log(`ERROR: Document id ${id} is not a valid DTMI.`) + return false + } if (!id.split(';')[0].startsWith(rootId.split(';')[0])) { - console.log(`ERROR: Document id ${id} does not satisfy the root id ${rootId}`) + console.log(`ERROR: Document id ${id} does not satisfy the root id ${rootId}.`) return false } } - console.log(`checkIds: validated ${ids.length} ids`) + console.log(`checkIds: Validated: ${ids.length} ids are under the root DTMI.`) return true } else { - console.log('checkIds: ids not found') + console.log('checkIds: Validated: Global ids not found.') return true } } -export /** +export/** * @description Checks if the folder/name convention matches the DTMI * @param {string} file * @returns {boolean} */ const checkDtmiPathFromFile = file => { - const model = JSON.parse(fs.readFileSync(file, 'utf-8')) + const model = JSON.parse(readFileSync(file, 'utf-8')) const id = model['@id'] if (id) { - const expectedPath = path.normalize(dtmi2path(model['@id'])) - if (file !== expectedPath) { - console.log(`ERROR: in current path ${file}, expecting ${expectedPath}.`) + const expectedPath = join(process.cwd(), dtmiToPath(model['@id']) || '') + if (resolve(file) !== expectedPath) { + console.log(`ERROR: in current path ${normalize(file)}, expecting ${expectedPath}.`) return false } else { console.log(`FilePath ${file} for ${id} seems OK.`) @@ -78,3 +127,41 @@ const checkDtmiPathFromFile = file => { return false } } + +export /** +* @param {string} dtmi +* @param {string | undefined} [repo] +* @param {undefined} [expanded] +* @returns {Array} +*/ +const resolveDtmi = async (dtmi, repo, expanded) => { + const result = [] + if (!repo) repo = 'https://' + 'devicemodels.azure.com' + const url = `${repo}${dtmiToPath(dtmi)}` + + if (expanded) { + const xurl = url.replace('.json', '.expanded.json') + const expRaw = await (await fetch(xurl)).text() + const exp = JSON.parse(expRaw) + exp.foreach(doc => { + const id = doc['@id'] + result.push({ id, doc }) + }) + return result + } + + const respJson = await (await fetch(url)).json() + const id = respJson['@id'] + if (id === dtmi) { + result.push({ id, respJson }) + const deps = getDependencies(respJson) + for await (const d of deps) { + const json = await (await fetch(`${repo}${dtmiToPath(d)}`)).json() + result.push({ d, json }) + } + } else { + console.error('ERR. Case diff ', id) + } + // console.log(result) + return result +} diff --git a/repo-convention.test.js b/repo-convention.test.js deleted file mode 100644 index 47a164f..0000000 --- a/repo-convention.test.js +++ /dev/null @@ -1,35 +0,0 @@ -import fs from 'fs' -import { dtmi2path, getDependencies, checkIds, checkDtmiPathFromFile } from './repo-convention.js' - -const readFile = file => { - return JSON.parse(fs.readFileSync(file, 'utf-8')) -} - -test('invalid dtmi', () => { - expect(dtmi2path('')).toBe('NOT-VALID-DTMI') - expect(dtmi2path('notadtmi')).toBe('NOT-VALID-DTMI') - expect(dtmi2path('dtmi:notadtmi')).toBe('NOT-VALID-DTMI') - expect(dtmi2path('dtmi:com:example:thermostat:1')).toBe('NOT-VALID-DTMI') - expect(dtmi2path('dtmi:com:example-bad:thermostat;1')).toBe('NOT-VALID-DTMI') -}) - -test('dtmi to path', () => { - expect(dtmi2path('dtmi:com:example:Thermostat;1')).toBe('dtmi/com/example/thermostat-1.json') - expect(dtmi2path('dtmi:com:Example:thermostat;1')).toBe('dtmi/com/example/thermostat-1.json') -}) - -test('get dependencies', () => { - expect(getDependencies(readFile('dtmi/test/uniqueids-1.json'))).toEqual([]) - expect(getDependencies(readFile('dtmi/test/onedep-1.json'))).toEqual(['dtmi:test:base;1']) - expect(getDependencies(readFile('dtmi/test/twodeps-1.json'))).toEqual(['dtmi:test:base;1', 'dtmi:test:onedep:comp1;1']) -}) - -test('check ids', () => { - expect(checkIds(readFile('dtmi/test/uniqueids-1.json'))).toBe(false) - expect(checkIds(readFile('dtmi/test/onedep-1.json'))).toBe(true) -}) - -test('checkDtmiPathFromFile', () => { - expect(checkDtmiPathFromFile('dtmi/test/uniqueids-1.json')).toBe(true) - expect(checkDtmiPathFromFile('dtmi/test/badpath.json')).toBe(false) -}) diff --git a/test/badpath.json b/test/badpath.json new file mode 100644 index 0000000..4d4f969 --- /dev/null +++ b/test/badpath.json @@ -0,0 +1,14 @@ +{ + "@context": "dtmi:dtdl:context;2", + "@id": "dtmi:test:onedep;1", + "@type": "Interface", + "displayName": "onedep", + "extends": ["dtmi:test:uniqueids;1"], + "contents": [ + { + "@type": "Component", + "name": "temperature", + "schema": "dtmi:test:onedep:comp1;1" + } + ] +} diff --git a/test/repo-convention.test.js b/test/repo-convention.test.js new file mode 100644 index 0000000..be11ad6 --- /dev/null +++ b/test/repo-convention.test.js @@ -0,0 +1,53 @@ +import { isDtmi, dtmiToPath, getDependencies, checkIds, checkDtmiPathFromFile, resolveDtmi } from '../repo-convention.js' +import { noDepsJson, oneDepJson, twoDepsJson, globalId } from './test-models' + +test('is valid dtmi', () => { + expect(isDtmi('dtmi:with::twosemicolons;1')).toBe(false) +}) + +test('invalid dtmi', () => { + expect(dtmiToPath('')).toBe(null) + expect(dtmiToPath('notadtmi')).toBe(null) + expect(dtmiToPath('dtmi:notadtmi')).toBe(null) + expect(dtmiToPath('dtmi:com:example:thermostat:1')).toBe(null) + expect(dtmiToPath('dtmi:com:example-bad:thermostat;1')).toBe(null) +}) + +test('dtmi to path', () => { + expect(dtmiToPath('dtmi:com:example:Thermostat;1')).toBe('/dtmi/com/example/thermostat-1.json') + expect(dtmiToPath('dtmi:com:Example:thermostat;1')).toBe('/dtmi/com/example/thermostat-1.json') +}) + +test('get dependencies', () => { + expect(getDependencies(noDepsJson)).toEqual([]) + expect(getDependencies(oneDepJson)).toEqual(['dtmi:test:base;1']) + expect(getDependencies(twoDepsJson)).toEqual(['dtmi:test:base;1', 'dtmi:test:onedep:comp1;1']) +}) + +test('check ids', () => { + expect(checkIds(globalId)).toBe(false) + expect(checkIds(oneDepJson)).toBe(true) +}) + +test('checkDtmiPathFromFile', () => { + expect(checkDtmiPathFromFile('dtmi/azure/devicemanagement/deviceinformation-1.json')).toBe(true) + expect(checkDtmiPathFromFile('test/badpath.json')).toBe(false) +}) + +test('resolveDtmi_noDeps', async () => { + const models = await resolveDtmi('dtmi:azure:DeviceManagement:DeviceInformation;1') + console.log(models.length) + expect(models.length).toBe(1) +}) + +test('resolveDtmi_Deps', async () => { + const models = await resolveDtmi('dtmi:com:example:TemperatureController;1') + console.log(models.length) + expect(models.length).toBe(3) +}) + +test('resolveDtmi_DepsExpanded', async () => { + const models = await resolveDtmi('dtmi:Espressif:SensorController;2', null, true) + console.log(models.length) + expect(models.length).toBe(6) +}) diff --git a/test/test-models.js b/test/test-models.js new file mode 100644 index 0000000..8d82e20 --- /dev/null +++ b/test/test-models.js @@ -0,0 +1,47 @@ +export const noDepsJson = { + '@context': 'dtmi:dtdl:context;2', + '@id': 'dtmi:test:onedep;1', + '@type': 'Interface', + displayName: 'onedep', + contents: [] +} + +export const oneDepJson = { + '@context': 'dtmi:dtdl:context;2', + '@id': 'dtmi:test:onedep;1', + '@type': 'Interface', + displayName: 'onedep', + extends: 'dtmi:test:base;1', + contents: [] +} + +export const globalId = { + '@context': 'dtmi:dtdl:context;2', + '@id': 'dtmi:test:twodeps;1', + '@type': 'Interface', + displayName: 'onedep', + extends: ['dtmi:test:base;1'], + contents: [ + { + '@id': 'dtmi:global;1', + '@type': 'Component', + name: 'temperature', + schema: 'dtmi:test:onedep:comp1;1' + } + ] +} + +export const twoDepsJson = { + '@context': 'dtmi:dtdl:context;2', + '@id': 'dtmi:test:twodeps;1', + '@type': 'Interface', + displayName: 'onedep', + extends: ['dtmi:test:base;1'], + contents: [ + { + '@type': 'Component', + name: 'temperature', + schema: 'dtmi:test:onedep:comp1;1' + } + ] +} diff --git a/tools/add-model.js b/tools/add-model.js new file mode 100644 index 0000000..4cc07c4 --- /dev/null +++ b/tools/add-model.js @@ -0,0 +1,55 @@ +import { readFileSync, existsSync, writeFileSync, unlinkSync } from 'fs' +import { join, dirname } from 'path' +import mkdirp from 'mkdirp' +import { dtmiToPath, checkIds, checkDependencies } from '../repo-convention.js' + +const createInterfaceFromFile = async file => { + const jsonDtdl = JSON.parse(readFileSync(file, 'utf-8')) + await createInterfaceFromJson(jsonDtdl) +} + +const createInterfaceFromJson = async jsonDtdl => { + const dtmi = jsonDtdl['@id'] + const fileName = join(process.cwd(), dtmiToPath(dtmi) || '') + if (existsSync(fileName)) { + console.log(`WARNING: ID ${dtmi} already exists at ${fileName} . Skipping `) + } else { + await mkdirp(dirname(fileName)) + writeFileSync(fileName, JSON.stringify(jsonDtdl, null, 2)) + console.log(`Model ${dtmi} added successfully to ${fileName}`) + } +} + +export/** + * @description Adds a model to the repo. Validates ids, dependencies and set the right folder/file name + * @param {string} file + */ +const addModel = async file => { + const rootJson = JSON.parse(readFileSync(file, 'utf-8')) + if (Array.isArray(rootJson)) { + for await (const d of rootJson) { + checkIds(d) + await createInterfaceFromJson(d) + } + return rootJson[0]['@id'] + } else { + checkIds(rootJson) + await createInterfaceFromFile(file) + return rootJson['@id'] + } +} + +const main = async () => { + const file = process.argv[2] + console.log(`processing: ${file}`) + const id = await addModel(file) + console.log('added', id) + if (id && !checkDependencies(id)) { + unlinkSync(join(process.cwd(), dtmiToPath(id) || '')) + console.log('ERROR: Dont forget to include all the dependencies before submitting.') + process.exit(1) + } + console.log(`SUCCESS: File ${file} added to ${dtmiToPath(id)}`) +} + +main() diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0c3af5f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "ES6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + "allowJs": true, /* Allow javascript files to be compiled. */ + "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +}