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
69 changes: 69 additions & 0 deletions packages/markdown-template/src/TemplateMarkTransformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ concept Thing {
o String[] items
}`;

const SCOPE_MODEL = `
namespace test@1.0.0
concept Address {
o String city
}
@template
concept TemplateData {
o String name
o Integer age optional
o String middleName optional
o Boolean isActive optional
o Address address optional
}`;

describe('#TemplateMarkTransformer', () => {
describe('#tokensToMarkdownTemplate', () => {
it('should handle join with type, style and locale', () => {
Expand Down Expand Up @@ -82,4 +96,59 @@ describe('#TemplateMarkTransformer', () => {
expect(joinNode.foo).toBeUndefined();
});
});

describe('#variables inside conditional and optional blocks', () => {
const parse = (content: string) => {
const transformer = new TemplateMarkTransformer();
const modelManager = new ModelManager();
modelManager.addCTOModel(SCOPE_MODEL);
return transformer.fromMarkdownTemplate({ content }, modelManager, 'clause');
};
const variables = (node: any): any[] => {
const own = node.$class === `${TemplateMarkModel.NAMESPACE}.VariableDefinition` ? [node] : [];
const children = ['nodes', 'whenTrue', 'whenFalse', 'whenSome', 'whenNone']
.flatMap((key) => node[key] || [])
.flatMap(variables);
return [...own, ...children];
};
const elementTypes = (content: string) =>
Object.fromEntries(variables(parse(content)).map((v) => [v.name, v.elementType]));

it.each([
['the condition property in #if', '{{#if age}}You are {{age}}.{{/if}}'],
['other properties in #if', '{{#if isActive}}Hello {{name}}, you are {{age}}.{{/if}}'],
['properties in the else branch of #if', '{{#if isActive}}Hello {{name}}.{{else}}Bye {{name}}.{{/if}}'],
['the optional property in #optional', '{{#optional middleName}}Middle {{middleName}}.{{else}}None.{{/optional}}'],
['other properties in #optional', '{{#optional age}}{{name}} is {{age}}.{{/optional}}'],
['properties in the else branch of #optional', '{{#optional age}}{{age}}.{{else}}Active: {{isActive}}.{{/optional}}'],
])('should resolve %s against the parent model', (_, content) => {
expect(() => parse(content)).not.toThrow();
});

it.each([
['#if', '{{#if age}}You are {{unknown}}.{{/if}}'],
['the else branch of #if', '{{#if isActive}}Hi.{{else}}Bye {{unknown}}.{{/if}}'],
['#optional', '{{#optional age}}You are {{unknown}}.{{/optional}}'],
['the else branch of #optional', '{{#optional age}}{{age}}.{{else}}{{unknown}}.{{/optional}}'],
])('should reject unknown properties in %s', (_, content) => {
expect(() => parse(content)).toThrow('Unknown property: unknown');
});

it('should type named variables inside #if by their own property', () => {
expect(elementTypes('{{#if isActive}}{{name}} is {{age}}.{{/if}}')).toEqual({
name: 'String',
age: 'Integer',
});
});

it('should resolve {{this}} to the guarded property', () => {
expect(elementTypes('{{#if age}}Age {{this}}.{{/if}}')).toEqual({ this: 'Integer' });
expect(elementTypes('{{#optional middleName}}Middle {{this}}.{{/optional}}')).toEqual({ this: 'String' });
});

it('should scope complex #optional blocks to the nested class', () => {
expect(elementTypes('{{#optional address}}Lives in {{city}}.{{/optional}}')).toEqual({ city: 'String' });
expect(() => parse('{{#optional address}}{{name}}{{/optional}}')).toThrow('Unknown property: name');
});
});
});
25 changes: 12 additions & 13 deletions packages/markdown-template/src/TypeVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ export class TypeVisitor {
_throwTemplateExceptionForElement('Unknown property: ' + thing.name, thing);
}
if (thing.name === 'this') {
const property = currentModel;
// Conditional and primitive optional blocks keep the parent scope,
// so {{this}} resolves to the guarded property instead
const property = parameters.thisProperty || currentModel;

if (property && property.getType) {
const serializer = parameters.templateMarkModelManager.getSerializer();
Expand Down Expand Up @@ -246,24 +248,24 @@ export class TypeVisitor {

const serializer = parameters.templateMarkModelManager.getSerializer();
thing.decorators = property ? processDecorators(serializer, property) : null;
const nextModel = property;
// Conditional blocks are guards and do not change scope
TypeVisitor.visitChildren(this, thing, {
templateMarkModelManager: parameters.templateMarkModelManager,
introspector: parameters.introspector,
model: nextModel,
model: currentModel,
thisProperty: property,
kind: parameters.kind,
}, 'whenTrue');
TypeVisitor.visitChildren(this, thing, {
templateMarkModelManager: parameters.templateMarkModelManager,
introspector: parameters.introspector,
model: null,
model: currentModel,
kind: parameters.kind,
}, 'whenFalse');
break;
}
case 'OptionalDefinition': {
const property = currentModel.getOwnProperty(thing.name);
let nextModel;
if (!property) {
_throwTemplateExceptionForElement('Unknown property: ' + thing.name, thing);
}
Expand All @@ -272,23 +274,20 @@ export class TypeVisitor {
}
const serializer = parameters.templateMarkModelManager.getSerializer();
thing.decorators = processDecorators(serializer, property);
if (property.isPrimitive()) {
thing.elementType = property.getFullyQualifiedTypeName();
nextModel = property;
} else {
thing.elementType = property.getFullyQualifiedTypeName();
nextModel = parameters.introspector.getClassDeclaration(thing.elementType);
}
thing.elementType = property.getFullyQualifiedTypeName();
// A primitive optional keeps the parent scope; a complex one scopes to its class
const nextModel = property.isPrimitive() ? currentModel : parameters.introspector.getClassDeclaration(thing.elementType);
TypeVisitor.visitChildren(this, thing, {
templateMarkModelManager: parameters.templateMarkModelManager,
introspector: parameters.introspector,
model: nextModel,
thisProperty: property.isPrimitive() ? property : null,
kind: parameters.kind,
}, 'whenSome');
TypeVisitor.visitChildren(this, thing, {
templateMarkModelManager: parameters.templateMarkModelManager,
introspector: parameters.introspector,
model: null,
model: currentModel,
kind: parameters.kind,
}, 'whenNone');
break;
Expand Down
Loading