Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Commit c08a09a

Browse files
author
JSpru
authored
Adding luis:version:export cmd (#356)
* Adding luis:version:export cmd * Remove dependency * Update example * Only store certain values in config, per specs * Refactor / keep logging and flag parsing in cmd only * Refactor write file error handling
1 parent 91881c3 commit c08a09a

File tree

4 files changed

+201
-3
lines changed

4 files changed

+201
-3
lines changed

packages/luis/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@types/chai": "^4.2.4",
2121
"@types/mocha": "^5.2.7",
2222
"@types/node": "^10.17.4",
23+
"@types/rimraf": "^2.0.3",
2324
"chai": "^4.2.0",
2425
"globby": "^10.0.1",
2526
"mocha": "^5.2.0",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import {CLIError, Command, flags} from '@microsoft/bf-cli-command'
7+
8+
const utils = require('../../../utils/index')
9+
10+
export default class LuisVersionExport extends Command {
11+
static description = 'Exports a LUIS application to JSON format'
12+
13+
static examples = [`
14+
$ bf luis:version:export --appId {APP_ID} --versionId {VERSION_ID} --out {FILENAME.json or PATH/FILENAME.json} --endpoint {ENDPOINT} --subscriptionKey {SUBSCRIPTION_KEY}
15+
`]
16+
17+
static flags = {
18+
help: flags.help({char: 'h'}),
19+
appId: flags.string({description: 'LUIS application Id'}),
20+
versionId: flags.string({description: 'LUIS application version Id'}),
21+
out: flags.string({char: 'o', description: 'Path to the directory where the exported file will be placed.'}),
22+
force: flags.boolean({char: 'f', description: 'If --out flag is provided with the path to an existing file, overwrites that file', default: false}),
23+
endpoint: flags.string({description: 'LUIS endpoint hostname'}),
24+
subscriptionKey: flags.string({description: 'LUIS cognitive services subscription key (aka Ocp-Apim-Subscription-Key)'}),
25+
}
26+
27+
async run() {
28+
const {flags} = this.parse(LuisVersionExport)
29+
const flagLabels = Object.keys(LuisVersionExport.flags)
30+
const configDir = this.config.configDir
31+
32+
let {
33+
appId,
34+
versionId,
35+
endpoint,
36+
force,
37+
out,
38+
subscriptionKey,
39+
} = await utils.processInputs(flags, flagLabels, configDir)
40+
41+
const requiredProps = {appId, versionId, endpoint, subscriptionKey}
42+
utils.validateRequiredProps(requiredProps)
43+
44+
const client = utils.getLUISClient(subscriptionKey, endpoint)
45+
46+
try {
47+
const appJSON = await client.versions.exportMethod(appId, versionId)
48+
if (!appJSON) throw new CLIError('Failed to export file')
49+
if (out) {
50+
const writtenFilePath: string = await utils.writeToFile(out, appJSON, force)
51+
this.log(`File successfully written: ${writtenFilePath}`)
52+
} else {
53+
await utils.writeToConsole(appJSON)
54+
this.log('App successfully exported\n')
55+
}
56+
} catch (error) {
57+
throw new CLIError(error)
58+
}
59+
}
60+
}

packages/luis/src/utils/index.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import {CLIError} from '@microsoft/bf-cli-command'
6+
import {CLIError, utils} from '@microsoft/bf-cli-command'
77
const path = require('path')
88
const fs = require('fs-extra')
99
const msRest = require('ms-rest')
@@ -36,17 +36,27 @@ const getLUISClient = (subscriptionKey: string, endpoint: string) => {
3636
return luisClient
3737
}
3838

39+
const filterByAllowedConfigValues = (configObj: any, prefix: string) => {
40+
const allowedConfigValues = [`${prefix}appId`, `${prefix}region`, `${prefix}subscriptionKey`, `${prefix}versionId`]
41+
const filtered = Object.keys(configObj)
42+
.filter(key => allowedConfigValues.includes(key))
43+
.reduce((filteredConfigObj: any, key) => {
44+
filteredConfigObj[key] = configObj[key]
45+
return filteredConfigObj
46+
}, {})
47+
return filtered
48+
}
49+
3950
const processInputs = async (flags: any, flagLabels: string[], configDir: string) => {
4051
const configPrefix = 'luis__'
41-
let config = await getUserConfig(configDir)
52+
let config = filterByAllowedConfigValues(await getUserConfig(configDir), configPrefix)
4253
config = config ? filterConfig(config, configPrefix) : config
4354
const input: any = {}
4455
flagLabels
4556
.filter(flag => flag !== 'help')
4657
.map((flag: string) => {
4758
input[flag] = flags[flag] || (config ? config[configPrefix + flag] : null)
4859
})
49-
5060
return input
5161
}
5262

@@ -58,7 +68,25 @@ const validateRequiredProps = (configObj: any) => {
5868
})
5969
}
6070

71+
const writeToConsole = (outputContents: string) => {
72+
const output = JSON.stringify(outputContents, null, 2)
73+
process.stdout.write(output, 'utf-8')
74+
}
75+
76+
const writeToFile = async (outputLocation: string, content: any, force: boolean) => {
77+
const validatedPath = utils.validatePath(outputLocation, '', force)
78+
try {
79+
await fs.ensureFile(outputLocation)
80+
await fs.writeJson(validatedPath, content, {spaces: 2})
81+
} catch (error) {
82+
throw new CLIError(error)
83+
}
84+
return validatedPath
85+
}
86+
6187
module.exports.getLUISClient = getLUISClient
6288
module.exports.getUserConfig = getUserConfig
6389
module.exports.processInputs = processInputs
6490
module.exports.validateRequiredProps = validateRequiredProps
91+
module.exports.writeToConsole = writeToConsole
92+
module.exports.writeToFile = writeToFile
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {expect, test} from '@oclif/test'
2+
const sinon = require('sinon')
3+
const uuidv1 = require('uuid/v1')
4+
const utils = require('../../../../src/utils/index')
5+
const fs = require('fs-extra')
6+
import * as rimraf from 'rimraf'
7+
8+
describe('luis:version:export', () => {
9+
10+
before(() => {
11+
fs.mkdirSync('./testout');
12+
});
13+
14+
after(() => {
15+
rimraf('./testout', (err) => {
16+
if (err) console.log(err);
17+
})
18+
});
19+
20+
beforeEach(() => {
21+
sinon.stub(utils, 'processInputs').returnsArg(0)
22+
})
23+
24+
afterEach(() => {
25+
sinon.restore();
26+
});
27+
28+
test
29+
.stdout()
30+
.command(['luis:version:export', '--help'])
31+
.it('should print the help contents when --help is passed as an argument', ctx => {
32+
expect(ctx.stdout).to.contain('Exports a LUIS application to JSON format')
33+
})
34+
35+
test
36+
.stdout()
37+
.stderr()
38+
.command(['luis:version:export', '--versionId', '0.1', '--subscriptionKey', uuidv1(), '--endpoint', 'https://westus.api.cognitive.microsoft.com'])
39+
.it('displays an error if any required input parameters are missing', ctx => {
40+
expect(ctx.stderr).to.contain(`Required input property 'appId' missing.`)
41+
})
42+
43+
test
44+
.stdout()
45+
.stderr()
46+
.command(['luis:version:export', '--appId', uuidv1(), '--endpoint', 'https://westus.api.cognitive.microsoft.com', '--subscriptionKey', uuidv1()])
47+
.it('displays an error if any required input parameters are missing', ctx => {
48+
expect(ctx.stderr).to.contain(`Required input property 'versionId' missing.`)
49+
})
50+
51+
test
52+
.nock('https://westus.api.cognitive.microsoft.com', api => api
53+
.get(uri => uri.includes('export'))
54+
.reply(200, {name: 'testname'})
55+
)
56+
.stdout()
57+
.command(['luis:version:export', '--appId', uuidv1(), '--versionId', '0.1', '--subscriptionKey', uuidv1(), '--endpoint', 'https://westus.api.cognitive.microsoft.com'])
58+
.it('exports a luis app and displays a success message and the export contents in the console', ctx => {
59+
expect(ctx.stdout).to.contain('App successfully exported')
60+
})
61+
62+
test
63+
.nock('https://westus.api.cognitive.microsoft.com', api => api
64+
.get(uri => uri.includes('export'))
65+
.reply(200, {name: 'testname'})
66+
)
67+
.stdout()
68+
.command(['luis:version:export', '--appId', uuidv1(), '--out', './testout/test.json', '--versionId', '0.1', '--subscriptionKey', uuidv1(), '--endpoint', 'https://westus.api.cognitive.microsoft.com'])
69+
.it('exports a luis app, displays a success message in the console and the export contents to the specified file', ctx => {
70+
expect(ctx.stdout).to.contain('File successfully written:')
71+
})
72+
73+
test
74+
.nock('https://westus.api.cognitive.microsoft.com', api => api
75+
.get(uri => uri.includes('export'))
76+
.reply(200, {name: 'testname'})
77+
)
78+
.stdout()
79+
.stderr()
80+
.command(['luis:version:export', '--appId', uuidv1(), '--out', 'xyz', '--versionId', '0.1', '--subscriptionKey', uuidv1(), '--endpoint', 'https://westus.api.cognitive.microsoft.com'])
81+
.it('exports a luis app and displays a success message and the export contents in the console (since the target path provided is invalid)', ctx => {
82+
expect(ctx.stderr).to.contain('Target directory path doesn\'t exist:')
83+
})
84+
85+
test
86+
.nock('https://westus.api.cognitive.microsoft.com', api => api
87+
.get(uri => uri.includes('export'))
88+
.reply(200, {name: 'testname'})
89+
)
90+
.stdout()
91+
.stderr()
92+
.command(['luis:version:export', '--appId', uuidv1(), '--out', './testout/test.json', '--force', '--versionId', '0.1', '--subscriptionKey', uuidv1(), '--endpoint', 'https://westus.api.cognitive.microsoft.com'])
93+
.it('exports a luis app, displays a success message in the console and the export contents to the specified file, overwriting the existing file of the same name', ctx => {
94+
expect(ctx.stdout).to.contain('File successfully written')
95+
})
96+
97+
test
98+
.nock('https://westus.api.cognitive.microsoft.com', api => api
99+
.get(uri => uri.includes('export'))
100+
.reply(200, {name: 'testname'})
101+
)
102+
.stdout()
103+
.command(['luis:version:export', '--appId', uuidv1(), '--out', './testout/test.json', '--versionId', '0.1', '--subscriptionKey', uuidv1(), '--endpoint', 'https://westus.api.cognitive.microsoft.com'])
104+
.it('exports a luis app, displays a success message in the console and the export contents to the specified file, incrementing the filename', ctx => {
105+
expect(ctx.stdout).to.contain('File successfully written')
106+
expect(ctx.stdout).to.contain('test(1).json')
107+
})
108+
109+
})

0 commit comments

Comments
 (0)