Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions repo-convention.d.ts → index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,11 @@ declare module '@ridomin/repo-scripts' {
* @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;
}
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { isDtmi, dtmiToPath, getDependencies, checkIds, checkDtmiPathFromFile } from './repo-convention.js'
17 changes: 16 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "@ridomin/repo-scripts",
"version": "0.0.10",
"version": "0.0.14",
"description": "",
"main": "repo-convention.js",
"type": "commonjs",
"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"
Expand All @@ -20,12 +21,14 @@
},
"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",
"mkdirp": "^1.0.4"
"mkdirp": "^1.0.4",
"node-fetch": "^2.6.1"
},
"standard": {
"globals": [
Expand Down
76 changes: 57 additions & 19 deletions repo-convention.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
const fs = require('fs')
const path = require('path')
const jsonata = require('jsonata')
import { readFileSync, existsSync } from 'fs'
import { join, resolve, normalize } from 'path'
import jsonata from 'jsonata'
import fetch from 'node-fetch'

/**
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<string>}
Expand Down Expand Up @@ -46,22 +47,22 @@ const getDependencies = rootJson => {
return deps
}

/**
export/**
* @description Checks all dependencies are available
* @param {Array<string>} deps
* @returns {boolean}
*/
const checkDependencies = dtmi => {
let result = true
const fileName = path.join(__dirname, dtmiToPath(dtmi))
const fileName = join(__dirname, dtmiToPath(dtmi) || '')
console.log(`Validating dependencies for ${dtmi} from ${fileName}`)
const dtdlJson = JSON.parse(fs.readFileSync(fileName, 'utf-8'))
const dtdlJson = JSON.parse(readFileSync(fileName, 'utf-8'))
const deps = getDependencies(dtdlJson)
deps.forEach(d => {
const fileName = path.join(__dirname, dtmiToPath(d))
if (fs.existsSync(fileName)) {
const fileName = join(__dirname, dtmiToPath(d) || '')
if (existsSync(fileName)) {
console.log(`Dependency ${d} found`)
const model = JSON.parse(fs.readFileSync(fileName, 'utf-8'))
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
Expand All @@ -74,7 +75,7 @@ const checkDependencies = dtmi => {
return result
}

/**
export/**
* @description Validates all internal IDs follow the namepspace set by the root id
* @param {any} dtdlJson
* @returns {boolean}
Expand Down Expand Up @@ -104,18 +105,18 @@ const checkIds = dtdlJson => {
}
}

/**
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.join(process.cwd(), dtmiToPath(model['@id']))
if (path.resolve(file) !== expectedPath) {
console.log(`ERROR: in current path ${path.normalize(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.`)
Expand All @@ -126,4 +127,41 @@ const checkDtmiPathFromFile = file => {
return false
}
}
module.exports = { dtmiToPath, isDtmi, checkIds, getDependencies, checkDependencies, checkDtmiPathFromFile }

export /**
* @param {string} dtmi
* @param {string | undefined} [repo]
* @param {undefined} [expanded]
* @returns {Array<string, string>}
*/
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
}
52 changes: 35 additions & 17 deletions test/repo-convention.test.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
const rc = require('../repo-convention.js')
const td = require('./test-models')
import { isDtmi, dtmiToPath, getDependencies, checkIds, checkDtmiPathFromFile, resolveDtmi } from '../repo-convention.js'
import { noDepsJson, oneDepJson, twoDepsJson, globalId } from './test-models'

test('is valid dtmi', () => {
expect(rc.isDtmi('dtmi:with::twosemicolons;1')).toBe(false)
expect(isDtmi('dtmi:with::twosemicolons;1')).toBe(false)
})

test('invalid dtmi', () => {
expect(rc.dtmiToPath('')).toBe(null)
expect(rc.dtmiToPath('notadtmi')).toBe(null)
expect(rc.dtmiToPath('dtmi:notadtmi')).toBe(null)
expect(rc.dtmiToPath('dtmi:com:example:thermostat:1')).toBe(null)
expect(rc.dtmiToPath('dtmi:com:example-bad:thermostat;1')).toBe(null)
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(rc.dtmiToPath('dtmi:com:example:Thermostat;1')).toBe('/dtmi/com/example/thermostat-1.json')
expect(rc.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')
expect(dtmiToPath('dtmi:com:Example:thermostat;1')).toBe('/dtmi/com/example/thermostat-1.json')
})

test('get dependencies', () => {
expect(rc.getDependencies(td.noDepsJson)).toEqual([])
expect(rc.getDependencies(td.oneDepJson)).toEqual(['dtmi:test:base;1'])
expect(rc.getDependencies(td.twoDepsJson)).toEqual(['dtmi:test:base;1', 'dtmi:test:onedep:comp1;1'])
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(rc.checkIds(td.globalId)).toBe(false)
expect(rc.checkIds(td.oneDepJson)).toBe(true)
expect(checkIds(globalId)).toBe(false)
expect(checkIds(oneDepJson)).toBe(true)
})

test('checkDtmiPathFromFile', () => {
expect(rc.checkDtmiPathFromFile('dtmi/azure/devicemanagement/deviceinformation-1.json')).toBe(true)
expect(rc.checkDtmiPathFromFile('test/badpath.json')).toBe(false)
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)
})
10 changes: 4 additions & 6 deletions test/test-models.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const noDepsJson = {
export const noDepsJson = {
'@context': 'dtmi:dtdl:context;2',
'@id': 'dtmi:test:onedep;1',
'@type': 'Interface',
displayName: 'onedep',
contents: []
}

const oneDepJson = {
export const oneDepJson = {
'@context': 'dtmi:dtdl:context;2',
'@id': 'dtmi:test:onedep;1',
'@type': 'Interface',
Expand All @@ -15,7 +15,7 @@ const oneDepJson = {
contents: []
}

const globalId = {
export const globalId = {
'@context': 'dtmi:dtdl:context;2',
'@id': 'dtmi:test:twodeps;1',
'@type': 'Interface',
Expand All @@ -31,7 +31,7 @@ const globalId = {
]
}

const twoDepsJson = {
export const twoDepsJson = {
'@context': 'dtmi:dtdl:context;2',
'@id': 'dtmi:test:twodeps;1',
'@type': 'Interface',
Expand All @@ -45,5 +45,3 @@ const twoDepsJson = {
}
]
}

module.exports = { noDepsJson, oneDepJson, twoDepsJson, globalId }
24 changes: 12 additions & 12 deletions tools/add-model.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
const fs = require('fs')
const path = require('path')
const mkdirp = require('mkdirp')
const { dtmiToPath, checkIds, checkDependencies } = require('../repo-convention.js')
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(fs.readFileSync(file, 'utf-8'))
const jsonDtdl = JSON.parse(readFileSync(file, 'utf-8'))
await createInterfaceFromJson(jsonDtdl)
}

const createInterfaceFromJson = async jsonDtdl => {
const dtmi = jsonDtdl['@id']
const fileName = path.join(process.cwd(), dtmiToPath(dtmi))
if (fs.existsSync(fileName)) {
const fileName = join(process.cwd(), dtmiToPath(dtmi) || '')
if (existsSync(fileName)) {
console.log(`WARNING: ID ${dtmi} already exists at ${fileName} . Skipping `)
} else {
await mkdirp(path.dirname(fileName))
fs.writeFileSync(fileName, JSON.stringify(jsonDtdl, null, 2))
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(fs.readFileSync(file, 'utf-8'))
const rootJson = JSON.parse(readFileSync(file, 'utf-8'))
if (Array.isArray(rootJson)) {
for await (const d of rootJson) {
checkIds(d)
Expand All @@ -45,7 +45,7 @@ const main = async () => {
const id = await addModel(file)
console.log('added', id)
if (id && !checkDependencies(id)) {
fs.unlinkSync(path.join(process.cwd(), dtmiToPath(id)))
unlinkSync(join(process.cwd(), dtmiToPath(id) || ''))
console.log('ERROR: Dont forget to include all the dependencies before submitting.')
process.exit(1)
}
Expand Down
Loading