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

Adding support for RoXMLElement #599

Merged
merged 11 commits into from
Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 2 additions & 0 deletions src/brsTypes/components/BrsObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RoDateTime } from "./RoDateTime";
import { Timespan } from "./Timespan";
import { createNodeByType } from "./RoSGNode";
import { RoRegex } from "./RoRegex";
import { RoXMLElement } from "./RoXMLElement";
import { BrsString, BrsBoolean } from "../BrsType";
import { RoString } from "./RoString";
import { roBoolean } from "./RoBoolean";
Expand Down Expand Up @@ -32,6 +33,7 @@ export const BrsObjects = new Map<string, Function>([
"roregex",
(_: Interpreter, expression: BrsString, flags: BrsString) => new RoRegex(expression, flags),
],
["roxmlelement", (_: Interpreter) => new RoXMLElement()],
["rostring", (_: Interpreter) => new RoString()],
["roboolean", (_: Interpreter, literal: BrsBoolean) => new roBoolean(literal)],
["rodouble", (_: Interpreter, literal: Double) => new roDouble(literal)],
Expand Down
92 changes: 92 additions & 0 deletions src/brsTypes/components/RoXMLElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { RoAssociativeArray } from "./RoAssociativeArray";
import { RoArray } from "./RoArray";
import { BrsComponent } from "./BrsComponent";
import { BrsBoolean, BrsString, BrsValue, ValueKind } from "../BrsType";
import { BrsType } from "..";
import { Callable, StdlibArgument } from "../Callable";
import { Interpreter } from "../../interpreter";
import { XmlDocument, XmlElement } from "xmldoc";

export class RoXMLElement extends BrsComponent implements BrsValue {
readonly kind = ValueKind.Object;
private xmlNode: any;

constructor() {
super("roXMLElement");

this.registerMethods({
ifXMLElement: [this.parse, this.getName, this.getNamedElementsCi, this.getAttributes],
});
}

toString(parent?: BrsType) {
return "<Component: roXMLElement>";
}

equalTo(other: BrsType) {
return BrsBoolean.False;
}

private parse = new Callable("parse", {
signature: {
args: [new StdlibArgument("str", ValueKind.String)],
returns: ValueKind.Boolean,
},
impl: (interpreter: Interpreter, str: BrsString) => {
try {
this.xmlNode = new XmlDocument(str.value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does XmlDocument return anything to indicate that the string was parsed correctly? something like a boolean value? or does it throws an exception in every case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It throws an exception only if the XML can not be parsed at all, but it doesn't if it can parse at least part of XML and it returns true in that case. The similar behavior we have on Roku.

return BrsBoolean.True;
} catch (err) {
this.xmlNode = undefined;
return BrsBoolean.False;
}
},
});

private getName = new Callable("getName", {
signature: {
args: [],
returns: ValueKind.String,
},
impl: (interpreter: Interpreter, str: BrsString) => {
return new BrsString(this.xmlNode?.name ?? "");
},
});

private getNamedElementsCi = new Callable("getNamedElementsCi", {
signature: {
args: [new StdlibArgument("str", ValueKind.String)],
returns: ValueKind.Object,
},
impl: (interpreter: Interpreter, str: BrsString) => {
let neededKey = str.value.toLowerCase();
let arr = [];
for (let item of this.xmlNode?.children ?? []) {
if (item.name?.toLowerCase() === neededKey) {
let childXmlElement = new RoXMLElement();
childXmlElement.xmlNode = item;
arr.push(childXmlElement);
}
}
return new RoArray(arr);
},
});

private getAttributes = new Callable("getAttributes", {
signature: {
args: [],
returns: ValueKind.Object,
},
impl: (interpreter: Interpreter, str: BrsString) => {
let array = [];
let attrs = this.xmlNode?.attr ?? {};
for (let key in attrs) {
array.push({
name: new BrsString(key),
value: new BrsString(attrs[key]),
});
}
return new RoAssociativeArray(array);
},
});
}
1 change: 1 addition & 0 deletions src/brsTypes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export * from "./components/RoAssociativeArray";
export * from "./components/Timespan";
export * from "./components/BrsObjects";
export * from "./components/RoRegex";
export * from "./components/RoXMLElement";
export * from "./components/RoString";
export * from "./components/RoBoolean";
export * from "./components/RoDouble";
Expand Down
100 changes: 100 additions & 0 deletions test/brsTypes/components/RoXMLElement.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const brs = require("brs");
const { BrsString, RoXMLElement } = brs.types;
const { Interpreter } = require("../../../lib/interpreter");

describe("RoXMLElement", () => {
let xmlParser;
let interpreter;

beforeEach(() => {
xmlParser = new RoXMLElement();
interpreter = new Interpreter();
});

describe("test methods for object with successful parsed xml", () => {
beforeEach(() => {
let parse = xmlParser.getMethod("parse");
parse.call(interpreter, new BrsString(getXmlString()));
});

it("getName", () => {
let getName = xmlParser.getMethod("getName");
expect(getName.call(interpreter)).toEqual(new BrsString("tag1"));
});

it("getNamedElementsCi", () => {
let getNamedElementsCi = xmlParser.getMethod("getNamedElementsCi");
expect(getNamedElementsCi.call(interpreter, new BrsString("any")).elements).toEqual([]);

let children = getNamedElementsCi.call(interpreter, new BrsString("CHiLd1"));
expect(children.elements.length).toEqual(2);

let getName = children.elements[0].getMethod("getName");
expect(getName.call(interpreter)).toEqual(new BrsString("Child1"));

getName = children.elements[1].getMethod("getName");
expect(getName.call(interpreter)).toEqual(new BrsString("CHILD1"));
});

it("getAttributes", () => {
let getAttributes = xmlParser.getMethod("getAttributes");
expect(getAttributes.call(interpreter).elements).not.toEqual(new Map());
expect(getAttributes.call(interpreter).elements).toEqual(
new Map([
["id", new BrsString("someId")],
["attr1", new BrsString("0")],
])
);
});
});

describe.each([
["test methods for object with no parsed xml", () => {}],
[
"test methods for object with failed parsing of xml",
() => {
let parse = xmlParser.getMethod("parse");
parse.call(interpreter, new BrsString('>bad_tag id="12" < some text >/bad_tag<'));
},
],
])("%s", (name, tryParse) => {
beforeEach(() => {
tryParse();
});

it("getName", () => {
let getName = xmlParser.getMethod("getName");
expect(getName).toBeTruthy();
expect(getName.call(interpreter)).toEqual(new BrsString(""));
});

it("getNamedElementsCi", () => {
let getNamedElementsCi = xmlParser.getMethod("getNamedElementsCi");
expect(getNamedElementsCi).toBeTruthy();
expect(getNamedElementsCi.call(interpreter, new BrsString("any")).elements).toEqual([]);
});

it("getAttributes", () => {
let getAttributes = xmlParser.getMethod("getAttributes");
expect(getAttributes).toBeTruthy();
expect(getAttributes.call(interpreter).elements).toEqual(new Map());
});
});

describe("test parse method with different xml strings", () => {
test.each([
["<tag>some text<tag>", true],
["<tag>some text <child1> child's text </child1> </tag>", true],
[getXmlString(), true],
['>bad_tag id="12" < some text >/bad_tag<', false],
["", false],
])("test parse with string %s", (xmlString, expected) => {
let parse = xmlParser.getMethod("parse");
expect(parse.call(interpreter, new BrsString(xmlString)).value).toBe(expected);
});
});
});

function getXmlString() {
return '<tag1 id="someId" attr1="0"> <Child1 id="id1"></Child1> <CHILD1 id="id2"></CHILD1> </tag1>';
}
29 changes: 29 additions & 0 deletions test/e2e/BrsComponents.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,35 @@ describe("end to end brightscript functions", () => {
]);
});

test.only("components/roXMLElement.brs", () => {
return execute([resourceFile("components", "roXMLElement.brs")], outputStreams).then(() => {
expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([
"xmlParser = ",
"<Component: roXMLElement>",
"type(xmlParser) = ",
"roXMLElement",
"parse bad xml string, result = ",
"false",
"parse good xml string, result = ",
"true",
"getName() = ",
"tag1",
"getAttributes() = ",
`<Component: roAssociativeArray> =\n` +
`{\n` +
` id: someId\n` +
` attr1: 0\n` +
`}`,
'getNamedElementsCi("child1") count = ',
"2",
"name of first child = ",
"Child1",
"mame of second child = ",
"CHILD1",
]);
});
});

test("components/customComponent.brs", async () => {
await execute([resourceFile("components", "customComponent.brs")], outputStreams);

Expand Down
13 changes: 13 additions & 0 deletions test/e2e/resources/components/roXMLElement.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
sub main()
xmlParser = createObject("roXMLElement")
?"xmlParser = "xmlParser
?"type(xmlParser) = "type(xmlParser)
?"parse bad xml string, result = "xmlParser.parse("some_xml_doc")
?"parse good xml string, result = "xmlParser.parse("<tag1 id=""someId"" attr1=""0""> <Child1 id=""id1""></Child1> <CHILD1 id=""id2""></CHILD1> </tag1>")
?"getName() = " xmlParser.getName()
?"getAttributes() = " xmlParser.getAttributes()
children = xmlParser.getNamedElementsCi("child1")
?"getNamedElementsCi(""child1"") count = " children.count()
?"name of first child = "children[0].getName()
?"mame of second child = "children[1].getName()
end sub