Skip to content

Commit

Permalink
[INTERNAL] Add ResourceTagCollection class
Browse files Browse the repository at this point in the history
Based on proposed RFC 0008:
SAP/ui5-tooling#243
  • Loading branch information
RandomByte committed Jul 20, 2020
1 parent dfe1c41 commit b0c7519
Show file tree
Hide file tree
Showing 2 changed files with 368 additions and 0 deletions.
88 changes: 88 additions & 0 deletions lib/ResourceTagCollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const tagNamespaceRegExp = new RegExp("^[a-z][a-z0-9]*$");
const tagNameRegExp = new RegExp("^[A-Z][A-Za-z0-9]+$"); // part after the namespace

const Resource = require("./Resource");

class ResourceTagCollection {
constructor({allowedTags}) {
if (!allowedTags || !allowedTags.length) {
throw new Error(`Missing parameter 'allowedTags'`);
}
// No validation of tag names here since we might remove/ignore
// this parameter in the future and generally allow all tags
this._allowedTags = Object.freeze(allowedTags);
this._pathTags = {};
}

setTag(resource, tag, value = true) {
this._validateResource(resource);
this._validateTag(tag);
this._validateValue(value);

const resourcePath = resource.getPath();
if (!this._pathTags[resourcePath]) {
this._pathTags[resourcePath] = {};
}
this._pathTags[resourcePath][tag] = value;
}

clearTag(resource, tag) {
this._validateResource(resource);
this._validateTag(tag);

const resourcePath = resource.getPath();
if (this._pathTags[resourcePath]) {
this._pathTags[resourcePath][tag] = undefined;
}
}

getTag(resource, tag) {
this._validateResource(resource);
this._validateTag(tag);

const resourcePath = resource.getPath();
if (this._pathTags[resourcePath]) {
return this._pathTags[resourcePath][tag];
}
}

_validateResource(resource) {
if (!(resource instanceof Resource)) {
throw new Error(`Invalid Resource: Must be instance of @ui5/fs.Resource`);
}
}

_validateTag(tag) {
if (!this._allowedTags.includes(tag)) {
throw new Error(`Invalid Tag: Not found in list of allowed tags. Allowed tags: ` +
this._allowedTags.join(", "));
}

if (!tag.includes(":")) {
throw new Error(`Invalid Tag: Colon required after namespace`);
}
const parts = tag.split(":");
if (parts.length > 2) {
throw new Error(`Invalid Tag: Expected exactly one colon but found ${parts.length - 1}`);
}

const [tagNamespace, tagName] = parts;
if (!tagNamespaceRegExp.test(tagNamespace)) {
throw new Error(
`Invalid Tag: Namespace part must be alphanumeric, lowercase and start with a letter`);
}
if (!tagNameRegExp.test(tagName)) {
throw new Error(`Invalid Tag: Name part must be alphanumeric and start with a capital letter`);
}
}

_validateValue(value) {
const type = typeof value;
if (!["string", "number", "boolean"].includes(type)) {
throw new Error(
`Invalid Tag Value: Must be of type string, number or boolean but is ${type}`);
}
}
}

module.exports = ResourceTagCollection;
280 changes: 280 additions & 0 deletions test/lib/ResourceTagCollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
const test = require("ava");
const sinon = require("sinon");

const Resource = require("../../lib/Resource");
const ResourceTagCollection = require("../../lib/ResourceTagCollection");

test.afterEach.always((t) => {
sinon.restore();
});

test("Constructor with missing allowedTags parameter", (t) => {
t.throws(() => {
new ResourceTagCollection({});
}, {
instanceOf: Error,
message: "Missing parameter 'allowedTags'"
});
});

test("setTag", (t) => {
const resource = new Resource({
path: "/some/path"
});
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
});

const validateResourceSpy = sinon.spy(tagCollection, "_validateResource");
const validateTagSpy = sinon.spy(tagCollection, "_validateTag");
const validateValueSpy = sinon.spy(tagCollection, "_validateValue");

tagCollection.setTag(resource, "abc:MyTag", "my value");

t.deepEqual(tagCollection._pathTags, {
"/some/path": {
"abc:MyTag": "my value"
}
}, "Tag correctly stored");

t.is(validateResourceSpy.callCount, 1, "_validateResource called once");
t.is(validateResourceSpy.getCall(0).args[0], resource,
"_validateResource called with correct arguments");

t.is(validateTagSpy.callCount, 1, "_validateTag called once");
t.is(validateTagSpy.getCall(0).args[0], "abc:MyTag",
"_validateTag called with correct arguments");

t.is(validateValueSpy.callCount, 1, "_validateValue called once");
t.is(validateValueSpy.getCall(0).args[0], "my value",
"_validateValue called with correct arguments");
});

test("setTag: Value defaults to true", (t) => {
const resource = new Resource({
path: "/some/path"
});
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
});
tagCollection.setTag(resource, "abc:MyTag");

t.deepEqual(tagCollection._pathTags, {
"/some/path": {
"abc:MyTag": true
}
}, "Tag correctly stored");
});

test("getTag", (t) => {
const resource = new Resource({
path: "/some/path"
});
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
});
tagCollection.setTag(resource, "abc:MyTag", 123);

const validateResourceSpy = sinon.spy(tagCollection, "_validateResource");
const validateTagSpy = sinon.spy(tagCollection, "_validateTag");

const value = tagCollection.getTag(resource, "abc:MyTag");

t.is(value, 123, "Got correct tag value");

t.is(validateResourceSpy.callCount, 1, "_validateResource called once");
t.is(validateResourceSpy.getCall(0).args[0], resource,
"_validateResource called with correct arguments");

t.is(validateTagSpy.callCount, 1, "_validateTag called once");
t.is(validateTagSpy.getCall(0).args[0], "abc:MyTag",
"_validateTag called with correct arguments");
});

test("clearTag", (t) => {
const resource = new Resource({
path: "/some/path"
});
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
});

tagCollection.setTag(resource, "abc:MyTag", 123);

const validateResourceSpy = sinon.spy(tagCollection, "_validateResource");
const validateTagSpy = sinon.spy(tagCollection, "_validateTag");

tagCollection.clearTag(resource, "abc:MyTag");

t.deepEqual(tagCollection._pathTags, {
"/some/path": {
"abc:MyTag": undefined
}
}, "Tag value set to undefined");

t.is(validateResourceSpy.callCount, 1, "_validateResource called once");
t.is(validateResourceSpy.getCall(0).args[0], resource,
"_validateResource called with correct arguments");

t.is(validateTagSpy.callCount, 1, "_validateTag called once");
t.is(validateTagSpy.getCall(0).args[0], "abc:MyTag",
"_validateTag called with correct arguments");
});

test("_validateTag: Not in list of allowed tags", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
});
t.throws(() => {
tagCollection._validateTag("abc:MyOtherTag");
}, {
instanceOf: Error,
message: "Invalid Tag: Not found in list of allowed tags. Allowed tags: abc:MyTag"
});
});

test("_validateTag: Missing colon", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["aBcMyTag"]
});
t.throws(() => {
tagCollection._validateTag("aBcMyTag");
}, {
instanceOf: Error,
message: "Invalid Tag: Colon required after namespace"
});
});

test("_validateTag: Too many colons", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["aBc:My:Tag"]
});
t.throws(() => {
tagCollection._validateTag("aBc:My:Tag");
}, {
instanceOf: Error,
message: "Invalid Tag: Expected exactly one colon but found 2"
});
});

test("_validateTag: Invalid namespace with uppercase letter", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["aBc:MyTag"]
});
t.throws(() => {
tagCollection._validateTag("aBc:MyTag");
}, {
instanceOf: Error,
message: "Invalid Tag: Namespace part must be alphanumeric, lowercase and start with a letter"
});
});

test("_validateTag: Invalid namespace starting with number", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["0abc:MyTag"]
});
t.throws(() => {
tagCollection._validateTag("0abc:MyTag");
}, {
instanceOf: Error,
message: "Invalid Tag: Namespace part must be alphanumeric, lowercase and start with a letter"
});
});

test("_validateTag: Invalid namespace containing an illegal character", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["a🦦c:MyTag"]
});
t.throws(() => {
tagCollection._validateTag("a🦦c:MyTag");
}, {
instanceOf: Error,
message: "Invalid Tag: Namespace part must be alphanumeric, lowercase and start with a letter"
});
});

test("_validateTag: Invalid tag name starting with number", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:0MyTag"]
});
t.throws(() => {
tagCollection._validateTag("abc:0MyTag");
}, {
instanceOf: Error,
message: "Invalid Tag: Name part must be alphanumeric and start with a capital letter"
});
});

test("_validateTag: Invalid tag name starting with lowercase letter", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:myTag"]
});
t.throws(() => {
tagCollection._validateTag("abc:myTag");
}, {
instanceOf: Error,
message: "Invalid Tag: Name part must be alphanumeric and start with a capital letter"
});
});

test("_validateTag: Invalid tag name containing an illegal character", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:My/Tag"]
});
t.throws(() => {
tagCollection._validateTag("abc:My/Tag");
}, {
instanceOf: Error,
message: "Invalid Tag: Name part must be alphanumeric and start with a capital letter"
});
});

test("_validateValue: Valid values", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
});
tagCollection._validateValue("bla");
tagCollection._validateValue("");
tagCollection._validateValue(true);
tagCollection._validateValue(false);
tagCollection._validateValue(123);
tagCollection._validateValue(0);
tagCollection._validateValue(NaN); // Is a number 🤷
t.pass("No exception thrown");
});

test("_validateValue: Invalid value of type object", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
});
t.throws(() => {
tagCollection._validateValue({foo: "bar"});
}, {
instanceOf: Error,
message: "Invalid Tag Value: Must be of type string, number or boolean but is object"
});
});

test("_validateValue: Invalid value undefined", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
});
t.throws(() => {
tagCollection._validateValue(undefined);
}, {
instanceOf: Error,
message: "Invalid Tag Value: Must be of type string, number or boolean but is undefined"
});
});

test("_validateValue: Invalid value null", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
});
t.throws(() => {
tagCollection._validateValue(null);
}, {
instanceOf: Error,
message: "Invalid Tag Value: Must be of type string, number or boolean but is object"
});
});

0 comments on commit b0c7519

Please sign in to comment.