Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NodeJS Templating] Enable host parameters #7199

Merged
merged 16 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from 12 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
57 changes: 33 additions & 24 deletions source/nodejs/adaptivecards-templating/src/template-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
import * as AEL from "adaptive-expressions";

class EvaluationContext {
private static readonly _reservedFields = ["$data", "$when", "$root", "$index"];
private static readonly _reservedFields = ["$data", "$when", "$root", "$index", "$host"];

private _stateStack: Array<{ $data: any, $index: any }> = [];
private _$data: any;

$root: any;
$host: any;
$index: number;

constructor(context?: IEvaluationContext) {
if (context !== undefined) {
this.$root = context.$root;
this.$host = context.$host;
}
}

Expand Down Expand Up @@ -55,6 +57,7 @@ class TemplateObjectMemory implements AEL.MemoryInterface {
$root: any;
$data: any;
$index: any;
$host: any;

constructor() {
this._memory = new AEL.SimpleObjectMemory(this);
Expand Down Expand Up @@ -84,24 +87,24 @@ export class GlobalSettings {
* and that field is undefined or null. By default, expression evaluation will substitute an undefined
* field with its binding expression (e.g. `${field}`). This callback makes it possible to customize that
* behavior.
*
*
* **Example**
* Given this data object:
*
*
* ```json
* {
* firstName: "David"
* }
* ```
*
*
* The expression `${firstName} ${lastName}` will evaluate to "David ${lastName}" because the `lastName`
* field is undefined.
*
*
* Now let's set the callback:
* ```typescript
* GlobalSettings.getUndefinedFieldValueSubstitutionString = (path: string) => { return "<undefined value>"; }
* ```
*
*
* With that, the above expression will evaluate to "David &lt;undefined value&gt;"
*/
static getUndefinedFieldValueSubstitutionString?: (path: string) => string | undefined = undefined;
Expand All @@ -115,7 +118,12 @@ export interface IEvaluationContext {
* The root data object the template will bind to. Expressions that refer to $root in the template payload
* map to this field. Initially, $data also maps to $root.
*/
$root: any
$root: any;
/**
* The host data object the template will bind to. Expressions that refer to $host in the template payload
* map to this field.
*/
$host?: any;
}

/**
Expand Down Expand Up @@ -157,6 +165,7 @@ export class Template {
memory.$root = context.$root;
memory.$data = context.$data;
memory.$index = context.$index;
memory.$host = context.$host;

let options: AEL.Options | undefined = undefined;

Expand All @@ -166,9 +175,9 @@ export class Template {
let substitutionValue: string | undefined = undefined;

if (GlobalSettings.getUndefinedFieldValueSubstitutionString) {
substitutionValue = GlobalSettings.getUndefinedFieldValueSubstitutionString(path);
substitutionValue = GlobalSettings.getUndefinedFieldValueSubstitutionString(path);
}

return substitutionValue ? substitutionValue : "${" + path + "}";
}
}
Expand All @@ -182,7 +191,7 @@ export class Template {

for (let childExpression of expression.children) {
let evaluationResult: { value: any; error: string };

try {
evaluationResult = childExpression.tryEvaluate(memory, options);
}
Expand All @@ -203,13 +212,13 @@ export class Template {

return { value: result, error: undefined };
}

return expression.tryEvaluate(memory, options);
}

/**
* Parses an interpolated string into an Expression object ready to evaluate.
*
*
* @param interpolatedString The interpolated string to parse. Example: "Hello ${name}"
* @returns An Expression object if the provided interpolated string contained at least one expression (e.g. "${expression}"); the original string otherwise.
*/
Expand Down Expand Up @@ -262,7 +271,7 @@ export class Template {

/**
* Tries to evaluate the provided expression using the provided context.
*
*
* @param expression The expression to evaluate.
* @param context The context (data) used to evaluate the expression.
* @param allowSubstitutions Indicates if the expression evaluator should substitute undefined value with a default
Expand Down Expand Up @@ -374,7 +383,7 @@ export class Template {
if (when instanceof AEL.Expression) {
let evaluationResult = Template.internalTryEvaluateExpression(when, this._context, false);
let whenValue: boolean = false;

// If $when fails to evaluate or evaluates to anything but a boolean, consider it is false
if (!evaluationResult.error) {
whenValue = typeof evaluationResult.value === "boolean" && evaluationResult.value;
Expand Down Expand Up @@ -412,8 +421,8 @@ export class Template {
* Initializes a new Template instance based on the provided payload.
* Once created, the instance can be bound to different data objects
* in a loop.
*
* @param payload The template payload.
*
* @param payload The template payload.
*/
constructor(payload: any) {
this._preparedPayload = Template.prepare(payload);
Expand All @@ -423,9 +432,9 @@ export class Template {
* Expands the template using the provided context. Template expansion involves
* evaluating the expressions used in the original template payload, as well as
* repeating (expanding) parts of that payload that are bound to arrays.
*
*
* Example:
*
*
* ```typescript
* let context = {
* $root: {
Expand All @@ -437,7 +446,7 @@ export class Template {
* ]
* }
* }
*
*
* let templatePayload = {
* type: "AdaptiveCard",
* version: "1.2",
Expand All @@ -453,14 +462,14 @@ export class Template {
* }
* ]
* }
*
*
* let template = new Template(templatePayload);
*
*
* let expandedTemplate = template.expand(context);
* ```
*
*
* With the above code, the value of `expandedTemplate` will be
*
*
* ```json
* {
* type: "AdaptiveCard",
Expand All @@ -481,7 +490,7 @@ export class Template {
* ]
* }
* ```
*
*
* @param context The context to bind the template to.
* @returns A value representing the expanded template. The type of that value
* is dependent on the type of the original template payload passed to the constructor.
Expand Down
3 changes: 2 additions & 1 deletion source/nodejs/lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"adaptivecards*/**",
"marked-schema/**",
"spec-generator/**",
"ac-typed-schema/**"
"ac-typed-schema/**",
"tests/unit-tests/**"
],
"command": {
"publish": {
Expand Down
3 changes: 2 additions & 1 deletion source/nodejs/tests/unit-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"devDependencies": {
"adaptivecards": "^2.10.0",
"monaco-editor": "^0.29.1",
"vkbeautify": "^0.99.3"
"vkbeautify": "^0.99.3",
"adaptivecards-templating": "^2.3.0-alpha.0"
}
}
99 changes: 99 additions & 0 deletions source/nodejs/tests/unit-tests/src/unit-tests.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as AdaptiveCards from "adaptivecards";
import * as ACData from "adaptivecards-templating";

var fs = require("fs");

describe("test Text property on TextBlock", () => {
it("UnitTest1", () => {
Expand All @@ -14,3 +17,99 @@ describe("test Text property on TextBlock", () => {
expect('{"type":"TextBlock","text":"Some text"}').toBe(JSON.stringify(textBlockJson));
});
});

describe("Test Templating Library", () => {
it("BasicTemplate", () => {
const templatePayload = {
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Hello ${name}!"
}
]
};

const root = {
"name": "Adaptive Cards"
};

const expectedOutput = {
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Hello Adaptive Cards!"
}
]
};

runTest(templatePayload, root, expectedOutput);
})

it("BasicTemplateWithHost", () => {
const templatePayload = {
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Hello ${name}! You're using the ${$host.WindowsTheme} theme!"
}
]
};

const root = {
"name": "Adaptive Cards"
};
const host = {
"WindowsTheme": "Light"
}

const expectedOutput = {
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Hello Adaptive Cards! You're using the Light theme!"
}
]
};

runTest(templatePayload, root, expectedOutput, host);
});

it("ComplexTemplate", () => {
runTest(loadFile("template-test-resources/complex-template.json"),
loadFile("template-test-resources/complex-template.data.json"),
loadFile("template-test-resources/complex-template.output.json"));
});

it("ComplexTemplateWithHost", () => {
runTest(loadFile("template-test-resources/complex-template-host.json"),
loadFile("template-test-resources/complex-template-host.data.json"),
loadFile("template-test-resources/complex-template-host.output.json"),
loadFile("template-test-resources/complex-template-host.host.json"));
});
});

function runTest(templatePayload: any, data: any, expectedOutput: any, host?: any) {
let template = new ACData.Template(templatePayload);

let context = {
$root: data,
$host: host
};

let card = template.expand(context);

expect(card).toStrictEqual(expectedOutput);
}

function loadFile(filePath: string) {
const dataObject = fs.readFileSync(filePath, "utf8");
return JSON.parse(dataObject);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"createdUtc": "2017-02-14T06:08:39Z",
"viewUrl": "https://adaptivecards.io",
"properties": [
{
"key": "Board",
"value": "Adaptive Cards"
},
{
"key": "List",
"value": "Backlog"
},
{
"key": "Assigned to",
"value": "Matt Hidinger"
},
{
"key": "Due date",
"value": "Not set"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"title": "Publish Adaptive Card Schema",
"description": "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.",
"creator": {
"name": "Matt Hidinger",
"profileImage": "https://pbs.twimg.com/profile_images/3647943215/d7f12830b3c17a5a9e4afcc370e3a37e_400x400.jpeg"
}
}
Loading