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

Commit 9b4dbc4

Browse files
munozemilioChris McConnell
andauthored
Dialog cherry pick of 932 and 944 (#945)
* Change dialog:merge bundling (#932) * Working bundle. * Figuring out $bundled for $ref * Remove $bundled. * Add tests for more complex schema. * Single process schema: Co-authored-by: Emilio Munoz <emmunozp@microsoft.com> * Sibling $ref required removed from definition. (#944) * Sibling $ref required removed from definition. * Update readme. Co-authored-by: Chris McConnell <chrimc@microsoft.com>
1 parent e3d7c37 commit 9b4dbc4

25 files changed

+4243
-508
lines changed

.vscode/launch.json

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@
138138
"libraries/**/*.schema",
139139
"-o",
140140
"${env:TEMP}/sdk.schema",
141-
"--verbose"
141+
"--verbose",
142+
"--debug"
142143
],
143144
"internalConsoleOptions": "openOnSessionStart",
144145
"cwd": "${workspaceFolder}/../botbuilder-dotnet"
@@ -193,12 +194,14 @@
193194
],
194195
"args": [
195196
"dialog:merge",
196-
"schemas/*.schema",
197+
"libraries/**/*.schema",
198+
"tests/**/*.schema",
197199
"-o",
198-
"schemas/app.schema"
200+
"tests/tests.schema",
201+
"--verbose"
199202
],
200203
"internalConsoleOptions": "openOnSessionStart",
201-
"cwd": "${workspaceFolder}/packages/dialog/test/commands/dialog"
204+
"cwd": "${workspaceFolder}/../botbuilder-dotnet/"
202205
},
203206
{
204207
"type": "node",
@@ -228,8 +231,8 @@
228231
"program": "${workspaceFolder}/packages/luis/bin/run",
229232
"outputCapture": "std",
230233
"outFiles": [
231-
"./packages/luis/lib/**",
232-
"./packages/lu/lib/**"
234+
"${workspaceFolder}/packages/luis/lib/**",
235+
"${workspaceFolder}/packages/lu/lib/**"
233236
],
234237
"args": [
235238
"luis:build",
@@ -239,7 +242,8 @@
239242
"${env:LUIS_AUTHORING_KEY}"
240243
],
241244
"internalConsoleOptions": "openOnSessionStart",
242-
"cwd": "${env:TEMP}/sandwich.out"
245+
"cwd": "${env:TEMP}/generate.out",
246+
"sourceMaps": true
243247
},
244248
{
245249
"type": "node",

packages/dialog/src/commands/dialog/merge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {Command, flags} from '@microsoft/bf-cli-command'
77
import SchemaMerger from '../../library/schemaMerger'
88

99
export default class DialogMerge extends Command {
10-
static description = 'Merge <kind>.schema and <kind>[.<locale>].uischema definitions from a project and its dependencies into a single .schema for describing .dialog files and a per locale .uischema for describing how Composer shows them. For C#, ensures all nuget declarative resources are included in the same location.'
10+
static description = 'Merge `<kind>.schema` and `<kind>[.<locale>].uischema` definitions from a project and its dependencies into a single .schema for describing .dialog files and a per locale .uischema for describing how Composer shows them. For C#, ensures all nuget declarative resources are included in the same location.'
1111

1212
static args = [
1313
{name: 'patterns', required: true, description: 'Any number of glob regex patterns to match .csproj, .nuspec or package.json files.'},

packages/dialog/src/library/schemaMerger.ts

Lines changed: 142 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,9 @@ export default class SchemaMerger {
441441
.map(kind => {
442442
return {$ref: `#/definitions/${kind}`}
443443
})
444-
this.addSchemaDefinitions()
444+
445+
// Add component schema definitions
446+
this.definitions = {...this.metaSchema.definitions, ...this.definitions}
445447

446448
if (!this.failed) {
447449
this.currentFile = this.output + '.schema'
@@ -463,7 +465,7 @@ export default class SchemaMerger {
463465
}
464466

465467
// Convert all remote references to local ones
466-
finalSchema = await parser.bundle(finalSchema as parser.JSONSchema, this.schemaProtocolResolver())
468+
await this.bundle(finalSchema)
467469
finalSchema = this.expandAllOf(finalSchema)
468470
this.removeId(finalSchema)
469471
if (this.debug) {
@@ -1333,20 +1335,132 @@ export default class SchemaMerger {
13331335
}
13341336
}
13351337

1336-
// Add schema definitions and turn schema: or full definition URI into local reference
1337-
private addSchemaDefinitions(): void {
1338-
const scheme = 'schema:'
1339-
this.definitions = {...this.metaSchema.definitions, ...this.definitions}
1340-
for (this.currentKind in this.definitions) {
1341-
walkJSON(this.definitions[this.currentKind], val => {
1342-
if (typeof val === 'object' && val.$ref && (val.$ref.startsWith(scheme) || val.$ref.startsWith(this.metaSchemaId))) {
1343-
val.$ref = val.$ref.substring(val.$ref.indexOf('#'))
1338+
// Split a $ref into path, pointer and name for definition
1339+
private splitRef(ref: string): {path: string, pointer: string, name: string} {
1340+
const hash = ref.indexOf('#')
1341+
const path = hash < 0 ? '' : ref.substring(0, hash)
1342+
const pointer = hash < 0 ? '' : ref.substring(hash + 1)
1343+
let name = ppath.basename(path)
1344+
if (name.endsWith('#')) {
1345+
name = name.substring(0, name.length - 1)
1346+
}
1347+
return {path, pointer, name}
1348+
}
1349+
1350+
// Bundle remote references into schema while pruning to minimally needed definitions.
1351+
// Remote references will be found under definitions/<pathBasename> which must be unique.
1352+
// There is special code to handle requires siblings to $ref where we remove the requires from
1353+
// the bundled definition. This is similar to what JSON schema 8 does.
1354+
private async bundle(schema: any): Promise<void> {
1355+
const current = this.currentFile
1356+
let sources: string[] = []
1357+
await this.bundleFun(schema, schema, sources, '')
1358+
for (let source of sources) {
1359+
this.prune(schema.definitions[source])
1360+
}
1361+
walkJSON(schema, elt => {
1362+
if (typeof elt === 'object') {
1363+
delete elt.$bundled
1364+
}
1365+
return false
1366+
})
1367+
this.currentFile = current
1368+
}
1369+
1370+
private async bundleFun(schema: any, elt: any, sources: string[], source: string): Promise<void> {
1371+
if (typeof elt === 'object' || Array.isArray(elt)) {
1372+
for (let key in elt) {
1373+
const val = elt[key]
1374+
if (key === '$ref' && typeof val === 'string') {
1375+
if (val.startsWith('schema:') || val.startsWith(this.metaSchemaId)) {
1376+
// Component schema reference
1377+
elt.$ref = val.substring(val.indexOf('#'))
1378+
} else {
1379+
const {path, pointer, name} = this.splitRef(val)
1380+
if (path) {
1381+
if (!schema.definitions[name]) {
1382+
// New source
1383+
this.currentFile = path
1384+
this.vlog(`Bundling ${path}`)
1385+
schema.definitions[name] = await getJSON(path)
1386+
sources.push(name)
1387+
}
1388+
let ref = `#/definitions/${name}${pointer}`
1389+
let definition: any = ptr.get(schema, ref)
1390+
if (!definition) {
1391+
this.refError(elt.$ref, ref)
1392+
} else if (!elt.$bundled) {
1393+
elt.$ref = ref
1394+
elt.$bundled = true
1395+
if (elt.required) {
1396+
// Strip required from destination
1397+
// This is to support a required sibling to $ref
1398+
delete definition.required
1399+
}
1400+
if (!definition.$bundled) {
1401+
// First outside reference mark it to keep and follow internal $ref
1402+
definition.$bundled = true
1403+
let cd = ''
1404+
try {
1405+
if (path.startsWith('file:')) {
1406+
cd = process.cwd()
1407+
process.chdir(ppath.dirname(path))
1408+
}
1409+
await this.bundleFun(schema, definition, sources, name)
1410+
} finally {
1411+
if (cd) {
1412+
process.chdir(cd)
1413+
}
1414+
}
1415+
}
1416+
}
1417+
} else if (source) {
1418+
// Internal reference in external source
1419+
const ref = `#/definitions/${source}${pointer}`
1420+
const definition: any = ptr.get(schema, ref)
1421+
if (!elt.$bundled) {
1422+
elt.$ref = ref
1423+
elt.$bundled = true
1424+
if (!definition.$bundled) {
1425+
definition.$bundled = true
1426+
await this.bundleFun(schema, definition, sources, source)
1427+
}
1428+
}
1429+
}
1430+
}
1431+
} else {
1432+
await this.bundleFun(schema, val, sources, source)
13441433
}
1345-
return false
1346-
})
1434+
}
13471435
}
13481436
}
13491437

1438+
// Prune out any unused keys inside of external schemas
1439+
private prune(elt: any): boolean {
1440+
let keep = false
1441+
if (typeof elt === 'object') {
1442+
keep = elt.$bundled
1443+
if (!keep) {
1444+
for (let [key, val] of Object.entries(elt)) {
1445+
if (typeof val === 'object' || Array.isArray(val)) {
1446+
let childBundled = this.prune(val)
1447+
if (!childBundled) {
1448+
// Prune any keys of unused structured object
1449+
delete elt[key]
1450+
}
1451+
keep = keep || childBundled
1452+
}
1453+
}
1454+
}
1455+
} else if (Array.isArray(elt)) {
1456+
for (let child of elt) {
1457+
const childKeep = this.prune(child)
1458+
keep = keep || childKeep
1459+
}
1460+
}
1461+
return keep
1462+
}
1463+
13501464
// Expand $ref below allOf and remove allOf
13511465
private expandAllOf(bundle: any): any {
13521466
walkJSON(bundle, val => {
@@ -1386,17 +1500,18 @@ export default class SchemaMerger {
13861500
this.currentKind = entry.$ref.substring(entry.$ref.lastIndexOf('/') + 1)
13871501
let definition = schema.definitions[this.currentKind]
13881502
let verifyProperty = (val, path) => {
1389-
if (!val.$schema) {
1390-
if (val.$ref) {
1391-
val = clone(val)
1392-
let ref: any = ptr.get(schema, val.$ref)
1393-
for (let prop in ref) {
1394-
if (!val[prop]) {
1395-
val[prop] = ref[prop]
1396-
}
1503+
if (val.$ref) {
1504+
val = clone(val)
1505+
let ref: any = ptr.get(schema, val.$ref)
1506+
for (let prop in ref) {
1507+
if (!val[prop]) {
1508+
val[prop] = ref[prop]
13971509
}
1398-
delete val.$ref
13991510
}
1511+
delete val.$ref
1512+
}
1513+
if (!val.$schema) {
1514+
// Assume $schema is an external reference and ignore error checking
14001515
if (val.$kind) {
14011516
let kind = schema.definitions[val.$kind]
14021517
if (this.roles(kind, 'interface').length > 0) {
@@ -1542,4 +1657,10 @@ export default class SchemaMerger {
15421657
this.error(`Error ${path} does not exist in schema`)
15431658
this.failed = true
15441659
}
1660+
1661+
// Missing $ref
1662+
private refError(original: string, modified: string): void {
1663+
this.error(`Error could not bundle ${original} into ${modified}`)
1664+
this.failed = true
1665+
}
15451666
}

packages/dialog/test/commands/dialog/merge.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ describe('dialog:merge', async () => {
180180
it('csproj-errors', async () => {
181181
console.log('\nStart csproj-errors')
182182
let [merged, lines] = await merge(['projects/project1/project1.csproj'], undefined, true)
183-
assert(!merged, 'Merging should faile')
183+
assert(!merged, 'Merging should fail')
184184
assert(countMatches(/error|warning/i, lines) === 3, 'Wrong number of errors or warnings')
185185
assert(countMatches(/Following.*project1/, lines) === 1, 'Did not follow project1')
186186
assert(countMatches(/Following nuget.*nuget1.*10.0.1/, lines) === 1, 'Did not follow nuget1')
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
3+
"$role": "implements(Microsoft.IDialog)",
4+
"title": "Adaptive Dialog",
5+
"description": "Flexible, data driven dialog that can adapt to the conversation.",
6+
"type": "object",
7+
"properties": {
8+
"id": {
9+
"type": "string",
10+
"title": "Id",
11+
"description": "Optional dialog ID."
12+
},
13+
"autoEndDialog": {
14+
"$ref": "schema:#/definitions/booleanExpression",
15+
"title": "Auto end dialog",
16+
"description": "If set to true the dialog will automatically end when there are no further actions. If set to false, remember to manually end the dialog using EndDialog action.",
17+
"default": true
18+
},
19+
"defaultResultProperty": {
20+
"type": "string",
21+
"title": "Default result property",
22+
"description": "Value that will be passed back to the parent dialog.",
23+
"default": "dialog.result"
24+
},
25+
"recognizer": {
26+
"$kind": "Microsoft.IRecognizer",
27+
"title": "Recognizer",
28+
"description": "Input recognizer that interprets user input into intent and entities."
29+
},
30+
"generator": {
31+
"$kind": "Microsoft.ILanguageGenerator",
32+
"title": "Language Generator",
33+
"description": "Language generator that generates bot responses."
34+
},
35+
"selector": {
36+
"$kind": "Microsoft.ITriggerSelector",
37+
"title": "Selector",
38+
"description": "Policy to determine which trigger is executed. Defaults to a 'best match' selector (optional)."
39+
},
40+
"triggers": {
41+
"type": "array",
42+
"description": "List of triggers defined for this dialog.",
43+
"title": "Triggers",
44+
"items": {
45+
"$kind": "Microsoft.ITrigger",
46+
"title": "Event triggers",
47+
"description": "Event triggers for handling events."
48+
}
49+
},
50+
"schema": {
51+
"title": "Schema",
52+
"description": "Schema to fill in.",
53+
"anyOf": [
54+
{
55+
"$ref": "http://json-schema.org/draft-07/schema#"
56+
},
57+
{
58+
"type": "string",
59+
"title": "Reference to JSON schema",
60+
"description": "Reference to JSON schema .dialog file."
61+
}
62+
]
63+
}
64+
}
65+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"$schema": "https://schemas.botframework.com/schemas/ui/v1.0/ui.schema",
3+
"form": {
4+
"label": "Adaptive dialog",
5+
"description": "This configures a data driven dialog via a collection of events and actions.",
6+
"helpLink": "https://aka.ms/bf-composer-docs-dialog",
7+
"order": [
8+
"recognizer",
9+
"*"
10+
],
11+
"hidden": [
12+
"triggers",
13+
"generator",
14+
"selector",
15+
"schema"
16+
],
17+
"properties": {
18+
"recognizer": {
19+
"label": "Language Understanding",
20+
"description": "To understand what the user says, your dialog needs a \"Recognizer\"; that includes example words and sentences that users may use."
21+
}
22+
}
23+
}
24+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
3+
"$role": [ "implements(Microsoft.IDialog)", "extends(Microsoft.SendActivity)" ],
4+
"title": "Send Activity to Ask a question",
5+
"description": "This is an action which sends an activity to the user when a response is expected",
6+
"type": "object",
7+
"properties": {
8+
"expectedProperties": {
9+
"$ref": "schema:#/definitions/arrayExpression",
10+
"title": "Expected Properties",
11+
"description": "Properties expected from the user.",
12+
"items": {
13+
"type": "string",
14+
"title": "Name",
15+
"description": "Name of the property"
16+
},
17+
"examples": [
18+
[
19+
"age",
20+
"name"
21+
]
22+
]
23+
},
24+
"defaultOperation": {
25+
"$ref": "schema:#/definitions/stringExpression",
26+
"title": "Default Operation",
27+
"description": "Sets the default operation that will be used when no operation is recognized in the response to this Ask.",
28+
"examples": [
29+
"Add()",
30+
"Remove()"
31+
]
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)