Skip to content
Open
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
166 changes: 133 additions & 33 deletions src/TemplateMarkInterpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,56 +460,134 @@ export class TemplateMarkInterpreter {
templateClass: ClassDeclaration;
clauseLibrary: object;

constructor(modelManager: ModelManager, clauseLibrary:object, templateConceptFqn?: string) {
constructor(
modelManager: ModelManager,
clauseLibrary: object,
templateConceptFqn?: string
) {
this.modelManager = modelManager;
this.clauseLibrary = clauseLibrary;
this.templateClass = getTemplateClassDeclaration(this.modelManager,templateConceptFqn);
this.templateClass = getTemplateClassDeclaration(
this.modelManager,
templateConceptFqn
);
}

/**
* Checks that a TemplateMark JSON document is valid with respect to the
* TemplateMark model, as well as the template model.
*
* Checks:
* 1. Variable names are valid properties in the template model
* 2. Optional properties have guards
* @param {*} templateMark the TemplateMark JSON object
* @returns {*} TemplateMark JSON that has been typed checked and has type metadata added
* @throws {Error} if the templateMark document is invalid
*/
* Checks that a TemplateMark JSON document is valid with respect to the
* TemplateMark model, as well as the template model.
*
* Checks:
* 1. Variable names are valid properties in the template model
* 2. Optional properties have guards
* @param {*} templateMark the TemplateMark JSON object
* @returns {*} TemplateMark JSON that has been typed checked and has type metadata added
* @throws {Error} if the templateMark document is invalid
*/
checkTypes(templateMark: object): object {
const modelManager = new ModelManager({ strict: true });
modelManager.addCTOModel(ConcertoMetaModel.MODEL, 'concertometamodel.cto');
modelManager.addCTOModel(CommonMarkModel.MODEL, 'commonmark.cto');
modelManager.addCTOModel(TemplateMarkModel.MODEL, 'templatemark.cto');
const factory = new Factory(modelManager);
const serializer = new Serializer(factory, modelManager);


try {
serializer.fromJSON(templateMark);
return templateMark;
}
catch(err) {
throw new Error(`Generated invalid agreement: ${err}: ${JSON.stringify(templateMark, null, 2)}`);
} catch (err) {
throw new Error(
`Generated invalid agreement: ${err}: ${JSON.stringify(
templateMark,
null,
2
)}`
);
}


const optionalProperties = new Set<string>();
const properties = this.templateClass.getProperties();
properties.forEach((prop) => {
if (prop.isOptional()) {
optionalProperties.add(prop.getName());
}
});


traverse(templateMark).forEach(function (node) {
if (
typeof node === 'object' &&
node.$class &&
typeof node.$class === 'string'
) {
const nodeClass = node.$class as string;

if (
VARIABLE_DEFINITION_RE.test(nodeClass) ||
ENUM_VARIABLE_DEFINITION_RE.test(nodeClass) ||
FORMATTED_VARIABLE_DEFINITION_RE.test(nodeClass)
) {
const varName = node.name;
if (optionalProperties.has(varName)) {

const path = this.path;
let isGuarded = false;

for (let i = path.length - 2; i >= 0; i--) {
const parent = traverse(templateMark).get(path.slice(0, i + 1));
if (parent && parent.$class) {
const parentClass = parent.$class as string;
if (
OPTIONAL_DEFINITION_RE.test(parentClass) ||
CONDITIONAL_DEFINITION_RE.test(parentClass)
) {
isGuarded = true;
break;
}

if (
CLAUSE_DEFINITION_RE.test(parentClass) ||
CONTRACT_DEFINITION_RE.test(parentClass)
) {
break;
}
}
}

if (!isGuarded) {
throw new Error(
`Optional property '${varName}' used in template without a guard (e.g., {{#optional ${varName}}} or {{#if ${varName}}}).`
);
}
}
}
}
});

return templateMark;
}

/**
* Compiles the code nodes containing TS to code nodes containing JS.
* @param {*} templateMark the TemplateMark JSON object
* @returns {*} TemplateMark JSON with JS nodes
* @throws {Error} if the templateMark document is invalid
*/
async compileTypeScriptToJavaScript(templateMark: object) : Promise<object> {
* Compiles the code nodes containing TS to code nodes containing JS.
* @param {*} templateMark the TemplateMark JSON object
* @returns {*} TemplateMark JSON with JS nodes
* @throws {Error} if the templateMark document is invalid
*/
async compileTypeScriptToJavaScript(templateMark: object): Promise<object> {
const templateConcept = (templateMark as any).nodes[0].elementType;
if(!templateConcept) {
if (!templateConcept) {
throw new Error('TemplateMark is not typed');
}
const compiler = new TemplateMarkToJavaScriptCompiler(this.modelManager, templateConcept);
const compiler = new TemplateMarkToJavaScriptCompiler(
this.modelManager,
templateConcept
);
await compiler.initialize();
return compiler.compile(templateMark);
}

validateCiceroMark(ciceroMark: object) : object {
validateCiceroMark(ciceroMark: object): object {
const modelManager = new ModelManager({ strict: true });
modelManager.addCTOModel(ConcertoMetaModel.MODEL, 'concertometamodel.cto');
modelManager.addCTOModel(CommonMarkModel.MODEL, 'commonmark.cto');
Expand All @@ -518,22 +596,44 @@ export class TemplateMarkInterpreter {
const serializer = new Serializer(factory, modelManager);
try {
return serializer.fromJSON(ciceroMark);
}
catch(err) {
throw new Error(`Generated invalid agreement: ${err}: ${JSON.stringify(ciceroMark, null, 2)}`);
} catch (err) {
throw new Error(
`Generated invalid agreement: ${err}: ${JSON.stringify(
ciceroMark,
null,
2
)}`
);
}
}

async generate(templateMark: object, data: TemplateData, options?:GenerationOptions): Promise<any> {
async generate(
templateMark: object,
data: TemplateData,
options?: GenerationOptions
): Promise<any> {
const factory = new Factory(this.modelManager);
const serializer = new Serializer(factory, this.modelManager);
const templateData = serializer.fromJSON(data);
if (templateData.getFullyQualifiedType() !== this.templateClass.getFullyQualifiedName()) {
throw new Error(`Template data must be of type '${this.templateClass.getFullyQualifiedName()}'.`);
if (
templateData.getFullyQualifiedType() !==
this.templateClass.getFullyQualifiedName()
) {
throw new Error(
`Template data must be of type '${this.templateClass.getFullyQualifiedName()}'.`
);
}
const typedTemplateMark = this.checkTypes(templateMark);
const jsTemplateMark = await this.compileTypeScriptToJavaScript(typedTemplateMark);
const ciceroMark = await generateAgreement(this.modelManager, this.clauseLibrary, jsTemplateMark, data, options);
const jsTemplateMark = await this.compileTypeScriptToJavaScript(
typedTemplateMark
);
const ciceroMark = await generateAgreement(
this.modelManager,
this.clauseLibrary,
jsTemplateMark,
data,
options
);
return this.validateCiceroMark(ciceroMark);
}
}
37 changes: 37 additions & 0 deletions test/issue-11.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ModelManager } from '@accordproject/concerto-core';
import { TemplateMarkInterpreter } from '../src/TemplateMarkInterpreter';
import { TemplateMarkTransformer } from '@accordproject/markdown-template';

describe('Issue #11: Unguarded Optional Variables', () => {
let modelManager: ModelManager;
let interpreter: TemplateMarkInterpreter;
let templateMarkTransformer: any;

beforeEach(async () => {
modelManager = new ModelManager({ strict: true });
const MODEL =
'namespace [email protected]\n@template\nconcept HelloWorld {\n o String name\n o String last optional\n}';
modelManager.addCTOModel(MODEL, 'hello.cto', true);
await modelManager.updateExternalModels();
interpreter = new TemplateMarkInterpreter(
modelManager,
{},
'[email protected]'
);
templateMarkTransformer = new TemplateMarkTransformer();
});

it('should throw compile-time error for unguarded optional variable', () => {
const TEMPLATE =
'Hello {{name}}!\nToday is **{{% return now.toISOString() %}}**.\n{{last}}';
const templateMark = templateMarkTransformer.fromMarkdownTemplate(
{ content: TEMPLATE, templateConceptFqn: '[email protected]' },
modelManager,
'contract',
{ verbose: false }
);
expect(() => interpreter.checkTypes(templateMark)).toThrow(
'Optional property \'last\' used in template without a guard (e.g., {{#optional last}} or {{#if last}}).'
);
});
});