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

Commit 46ecaa3

Browse files
Chris McConnellmunozemilio
andauthored
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 <[email protected]>
1 parent fedc732 commit 46ecaa3

21 files changed

+2244
-26
lines changed

.vscode/launch.json

Lines changed: 6 additions & 4 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"
@@ -228,8 +229,8 @@
228229
"program": "${workspaceFolder}/packages/luis/bin/run",
229230
"outputCapture": "std",
230231
"outFiles": [
231-
"./packages/luis/lib/**",
232-
"./packages/lu/lib/**"
232+
"${workspaceFolder}/packages/luis/lib/**",
233+
"${workspaceFolder}/packages/lu/lib/**"
233234
],
234235
"args": [
235236
"luis:build",
@@ -239,7 +240,8 @@
239240
"${env:LUIS_AUTHORING_KEY}"
240241
],
241242
"internalConsoleOptions": "openOnSessionStart",
242-
"cwd": "${env:TEMP}/sandwich.out"
243+
"cwd": "${env:TEMP}/generate.out",
244+
"sourceMaps": true
243245
},
244246
{
245247
"type": "node",

packages/dialog/src/library/schemaMerger.ts

Lines changed: 135 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,18 +1335,123 @@ 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+
private async bundle(schema: any): Promise<void> {
1353+
const current = this.currentFile
1354+
let sources: string[] = []
1355+
await this.bundleFun(schema, schema, sources, '')
1356+
for (let source of sources) {
1357+
this.prune(schema.definitions[source])
1358+
}
1359+
walkJSON(schema, elt => {
1360+
if (typeof elt === 'object') {
1361+
delete elt.$bundled
1362+
}
1363+
return false
1364+
})
1365+
this.currentFile = current
1366+
}
1367+
1368+
private async bundleFun(schema: any, elt: any, sources: string[], source: string): Promise<void> {
1369+
if (typeof elt === 'object' || Array.isArray(elt)) {
1370+
for (let key in elt) {
1371+
const val = elt[key]
1372+
if (key === '$ref' && typeof val === 'string') {
1373+
if (val.startsWith('schema:') || val.startsWith(this.metaSchemaId)) {
1374+
// Component schema reference
1375+
elt.$ref = val.substring(val.indexOf('#'))
1376+
} else {
1377+
const {path, pointer, name} = this.splitRef(val)
1378+
if (path) {
1379+
if (!schema.definitions[name]) {
1380+
// New source
1381+
this.currentFile = path
1382+
this.vlog(`Bundling ${path}`)
1383+
schema.definitions[name] = await getJSON(path)
1384+
sources.push(name)
1385+
}
1386+
let ref = `#/definitions/${name}${pointer}`
1387+
let definition: any = ptr.get(schema, ref)
1388+
if (!definition) {
1389+
this.refError(elt.$ref, ref)
1390+
} else if (!elt.$bundled) {
1391+
elt.$ref = ref
1392+
elt.$bundled = true
1393+
if (!definition.$bundled) {
1394+
// First outside reference mark it to keep and follow internal $ref
1395+
definition.$bundled = true
1396+
let cd = ''
1397+
try {
1398+
if (path.startsWith('file:')) {
1399+
cd = process.cwd()
1400+
process.chdir(ppath.dirname(path))
1401+
}
1402+
await this.bundleFun(schema, definition, sources, name)
1403+
} finally {
1404+
if (cd) {
1405+
process.chdir(cd)
1406+
}
1407+
}
1408+
}
1409+
}
1410+
} else if (source) {
1411+
// Internal reference in external source
1412+
const ref = `#/definitions/${source}${pointer}`
1413+
const definition: any = ptr.get(schema, ref)
1414+
if (!elt.$bundled) {
1415+
elt.$ref = ref
1416+
elt.$bundled = true
1417+
if (!definition.$bundled) {
1418+
definition.$bundled = true
1419+
await this.bundleFun(schema, definition, sources, source)
1420+
}
1421+
}
1422+
}
1423+
}
1424+
} else {
1425+
await this.bundleFun(schema, val, sources, source)
13441426
}
1345-
return false
1346-
})
1427+
}
1428+
}
1429+
}
1430+
1431+
// Prune out any unused keys inside of external schemas
1432+
private prune(elt: any): boolean {
1433+
let keep = false
1434+
if (typeof elt === 'object') {
1435+
keep = elt.$bundled
1436+
if (!keep) {
1437+
for (let [key, val] of Object.entries(elt)) {
1438+
if (typeof val === 'object' || Array.isArray(val)) {
1439+
let childBundled = this.prune(val)
1440+
if (!childBundled) {
1441+
// Prune any keys of unused structured object
1442+
delete elt[key]
1443+
}
1444+
keep = keep || childBundled
1445+
}
1446+
}
1447+
}
1448+
} else if (Array.isArray(elt)) {
1449+
for (let child of elt) {
1450+
const childKeep = this.prune(child)
1451+
keep = keep || childKeep
1452+
}
13471453
}
1454+
return keep
13481455
}
13491456

13501457
// Expand $ref below allOf and remove allOf
@@ -1386,17 +1493,18 @@ export default class SchemaMerger {
13861493
this.currentKind = entry.$ref.substring(entry.$ref.lastIndexOf('/') + 1)
13871494
let definition = schema.definitions[this.currentKind]
13881495
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-
}
1496+
if (val.$ref) {
1497+
val = clone(val)
1498+
let ref: any = ptr.get(schema, val.$ref)
1499+
for (let prop in ref) {
1500+
if (!val[prop]) {
1501+
val[prop] = ref[prop]
13971502
}
1398-
delete val.$ref
13991503
}
1504+
delete val.$ref
1505+
}
1506+
if (!val.$schema) {
1507+
// Assume $schema is an external reference and ignore error checking
14001508
if (val.$kind) {
14011509
let kind = schema.definitions[val.$kind]
14021510
if (this.roles(kind, 'interface').length > 0) {
@@ -1542,4 +1650,10 @@ export default class SchemaMerger {
15421650
this.error(`Error ${path} does not exist in schema`)
15431651
this.failed = true
15441652
}
1653+
1654+
// Missing $ref
1655+
private refError(original: string, modified: string): void {
1656+
this.error(`Error could not bundle ${original} into ${modified}`)
1657+
this.failed = true
1658+
}
15451659
}

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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
3+
"title": "Microsoft Dialogs",
4+
"description": "Components which derive from Dialog",
5+
"$role": "interface",
6+
"oneOf": [
7+
{
8+
"type": "string"
9+
}
10+
]
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
3+
"title": "Microsoft LanguageGenerator",
4+
"description": "Components which dervie from the LanguageGenerator class",
5+
"$role": "interface",
6+
"oneOf": [
7+
{
8+
"type": "string"
9+
}
10+
]
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
3+
"title": "Microsoft Recognizer",
4+
"description": "Components which derive from Recognizer class",
5+
"$role": "interface",
6+
"oneOf": [
7+
{
8+
"type": "string"
9+
}
10+
]
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
3+
"$role": "interface",
4+
"title": "Microsoft Triggers",
5+
"description": "Components which derive from OnCondition class."
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema",
3+
"$role": "interface",
4+
"title": "Selectors",
5+
"description": "Components which derive from TriggerSelector class."
6+
}

0 commit comments

Comments
 (0)