Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
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
15 changes: 6 additions & 9 deletions Composer/packages/client/src/store/reducer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import get from 'lodash/get';
import set from 'lodash/set';
import merge from 'lodash/merge';
import memoize from 'lodash/memoize';
import { indexer, dialogIndexer, lgIndexer, luIndexer, autofixReferInDialog } from '@bfc/indexers';
import {
SensitiveProperties,
Expand All @@ -30,14 +31,10 @@ import createReducer from './createReducer';

const projectFiles = ['bot', 'botproj'];

const processSchema = schema => {
const resolvedDefs = dereferenceDefinitions(schema.definitions);

return {
...schema,
definitions: resolvedDefs,
};
};
const processSchema = memoize((projectId: string, schema: any) => ({
...schema,
definitions: dereferenceDefinitions(schema.definitions),
}));

// if user set value in terminal or appsetting.json, it should update the value in localStorage
const refreshLocalStorage = (botName: string, settings: DialogSetting) => {
Expand Down Expand Up @@ -85,7 +82,7 @@ const initLuFilesStatus = (botName: string, luFiles: LuFile[], dialogs: DialogIn

const getProjectSuccess: ReducerFunc = (state, { response }) => {
const { files, botName, botEnvironment, location, schemas, settings, id, locale } = response.data;
schemas.sdk.content = processSchema(schemas.sdk.content);
schemas.sdk.content = processSchema(id, schemas.sdk.content);
const { dialogs, luFiles, lgFiles, skillManifestFiles } = indexer.index(files, botName, schemas.sdk.content, locale);
state.projectId = id;
state.dialogs = dialogs;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { isCircular, CIRCULAR_REFS } from '../../src/schemaUtils/circular';

describe('isCircular', () => {
it('returns true for kinds in CIRCULAR_REFS', () => {
CIRCULAR_REFS.forEach(k => {
expect(isCircular(k)).toBe(true);
});
});

it('matches on internal refs', () => {
expect(isCircular('Microsoft.AdaptiveDialog/properties/schema/anyOf/0')).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { dereference, dereferenceDefinitions } from '../../src/schemaUtils/dereference';
import { SchemaDefinitions } from '../../src/schemaUtils/types';

const defs: SchemaDefinitions = {
'Microsoft.IDialog': {
oneOf: [
{
$ref: '#/definitions/Microsoft.Foo',
},
{
$ref: '#/definitions/Microsoft.Bar',
},
],
},
'Microsoft.Foo': {
properties: {
actions: {
type: 'array',
items: {
$ref: '#/definitions/Microsoft.IDialog',
},
},
},
},
'Microsoft.Bar': {
properties: {
name: {
title: 'name',
type: 'string',
},
num: {
title: 'num',
oneOf: [
{
title: 'float',
$ref: '#/definitions/num',
},
{
type: 'integer',
},
],
},
},
},
num: {
type: 'number',
},
};

describe('dereference', () => {
const cache = new Map();

it('dereferences arrays and object', () => {
const result = dereference(defs['Microsoft.Bar'], defs, cache);
expect(result).toEqual({
properties: {
name: {
title: 'name',
type: 'string',
},
num: {
title: 'num',
oneOf: [
{
title: 'float',
type: 'number',
},
{
type: 'integer',
},
],
},
},
});
});

it('does not dereference circular refs', () => {
const result = dereference(defs['Microsoft.IDialog'], defs, cache);
expect(result).toEqual({
oneOf: [
{
properties: {
actions: {
type: 'array',
items: {
$ref: '#/definitions/Microsoft.IDialog',
},
},
},
},
{
properties: {
name: {
title: 'name',
type: 'string',
},
num: {
title: 'num',
oneOf: [
{
title: 'float',
type: 'number',
},
{
type: 'integer',
},
],
},
},
},
],
});
});
});

describe('dereferenceDefinitions', () => {
it('dereferences all definitions except ones with circular refs', () => {
expect(dereferenceDefinitions(defs)).toEqual({
'Microsoft.IDialog': {
oneOf: [
{
$ref: '#/definitions/Microsoft.Foo',
},
{
$ref: '#/definitions/Microsoft.Bar',
},
],
},
'Microsoft.Foo': {
properties: {
actions: {
type: 'array',
items: {
$ref: '#/definitions/Microsoft.IDialog',
},
},
},
},
'Microsoft.Bar': {
properties: {
name: {
title: 'name',
type: 'string',
},
num: {
title: 'num',
oneOf: [
{
title: 'float',
type: 'number',
},
{
type: 'integer',
},
],
},
},
},
num: {
type: 'number',
},
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { getRef } from '../../src/schemaUtils/getRef';
import { dereference } from '../../src/schemaUtils/dereference';
import { SchemaDefinitions } from '../../src/schemaUtils/types';

jest.mock('../../src/schemaUtils/dereference', () => ({
dereference: jest.fn(),
}));

const defs: SchemaDefinitions = {
'Microsoft.IDialog': {
oneOf: [
{
$ref: '#/definitions/Microsoft.Foo',
},
{
$ref: '#/definitions/Microsoft.Bar',
},
],
},
'Microsoft.Foo': {
properties: {
actions: {
type: 'array',
items: {
$ref: '#/definitions/Microsoft.IDialog',
},
},
},
},
'Microsoft.Bar': {
properties: {
name: {
type: 'string',
},
},
},
};

beforeEach(() => {
(dereference as jest.Mock).mockClear();
});

describe('getRef', () => {
describe('when there is a cache hit', () => {
const cache = new Map([['Microsoft.Bar', 'cache value']]);

it('returns the item in the cache', () => {
expect(getRef('#/definitions/Microsoft.Bar', defs, cache)).toEqual('cache value');
});

it('does not attempt to dereference', () => {
getRef('#/definitions/Microsoft.Bar', defs, cache);
expect(dereference).not.toHaveBeenCalled();
});
});

describe('when there is a cache miss', () => {
const emptyCache = new Map();

it('throws an error when the definition does not exist', () => {
expect(() => getRef('#/definitions/Foobar', defs, emptyCache)).toThrow('Definition not found for Foobar');
});

describe('when a definition is found', () => {
it('dereferences if def is not a circular reference', () => {
(dereference as jest.Mock).mockReturnValue('dereferenced def');
expect(getRef('#/definitions/Microsoft.Foo', defs, emptyCache)).toEqual('dereferenced def');
});

it('updates the cache with the resolved ref', () => {
(dereference as jest.Mock).mockReturnValue('dereferenced def');
getRef('#/definitions/Microsoft.Foo', defs, emptyCache);
expect(emptyCache.get('Microsoft.Foo')).toEqual('dereferenced def');
});

it('does not attempt to dereference if def is a circular reference', () => {
getRef('#/definitions/Microsoft.IDialog', defs, emptyCache);
expect(dereference).not.toHaveBeenCalled();
});
});
});
});
14 changes: 14 additions & 0 deletions Composer/packages/lib/shared/src/schemaUtils/circular.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import memoize from 'lodash/memoize';

export const CIRCULAR_REFS = [
'Microsoft.IDialog',
'Microsoft.IRecognizer',
'Microsoft.ILanguageGenerator',
'Microsoft.ITriggerSelector',
'Microsoft.AdaptiveDialog',
];

export const isCircular = memoize((def: string) => CIRCULAR_REFS.some(kind => def.includes(kind)));
Loading