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

[FEATURE] Introduce Readers "Filter" and "Transformer" #331

Merged
merged 15 commits into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 8 additions & 0 deletions lib/AbstractReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ class AbstractReader {
});
}

filter(callback) {
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
const ReaderFilter = require("./ReaderFilter");
Copy link
Member

Choose a reason for hiding this comment

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

Note: Cyclic dependency
This shouldn't cause any issues but we might also just use the "ReaderFilter" directly within ui5-builder.

return new ReaderFilter({
reader: this,
filterCallback: callback
});
}

/**
* Locates resources by one or more glob patterns.
*
Expand Down
75 changes: 75 additions & 0 deletions lib/ReaderFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const AbstractReader = require("./AbstractReader");

/**
* Transparently apply filters on resource readers by wrapping them.
*
* @public
* @memberof module:@ui5/fs
* @augments module:@ui5/fs.AbstractReader
*/
class ReaderFilter extends AbstractReader {
/**
* Filter callback
*
* @public
* @callback module:@ui5/fs.ReaderFilter~filterCallback
* @param {module:@ui5/fs.Resource} resource Resource to test
* @returns {boolean} Return <code>true</code> to keep the resource and <code>false</code> to disregard it
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
*/

/**
* Constructor
*
* @param {object} parameters Parameters
* @param {module:@ui5/fs.AbstractReader} parameters.reader The resource reader to wrap
* @param {module:@ui5/fs.ReaderFilter~filterCallback} parameters.filterCallback
* Filter function. Will be called for every resource read through this reader.
*/
constructor({reader, filterCallback}) {
super();
if (!reader) {
throw new Error(`Missing parameter "reader"`);
}
if (!filterCallback) {
throw new Error(`Missing parameter "filterCallback"`);
}
this._reader = reader;
this._filterCallback = filterCallback;
}

/**
* Locates resources by glob.
*
* @private
* @param {string|string[]} pattern glob pattern as string or an array of
* glob patterns for virtual directory structure
* @param {object} options glob options
* @param {module:@ui5/fs.tracing.Trace} trace Trace instance
* @returns {Promise<module:@ui5/fs.Resource[]>} Promise resolving to list of resources
*/
async _byGlob(pattern, options, trace) {
const result = await this._reader._byGlob(pattern, options, trace);
return result.filter(this._filterCallback);
}

/**
* Locates resources by path.
*
* @private
* @param {string} virPath Virtual path
* @param {object} options Options
* @param {module:@ui5/fs.tracing.Trace} trace Trace instance
* @returns {Promise<module:@ui5/fs.Resource>} Promise resolving to a single resource
*/
async _byPath(virPath, options, trace) {
const result = await this._reader._byPath(virPath, options, trace);
if (result) {
if (!this._filterCallback(result)) {
return null;
}
}
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
return result;
}
}

module.exports = ReaderFilter;
26 changes: 25 additions & 1 deletion lib/ResourceTagCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,29 @@ const tagNamespaceRegExp = new RegExp("^[a-z][a-z0-9]*$"); // part before the co
const tagNameRegExp = new RegExp("^[A-Z][A-Za-z0-9]+$"); // part after the colon

class ResourceTagCollection {
constructor({allowedTags}) {
constructor({allowedTags, superCollection}) {
if (!allowedTags || !allowedTags.length) {
throw new Error(`Missing parameter 'allowedTags'`);
}

if (superCollection) {
this._superCollection = superCollection;
this._superTags = this._superCollection.getAcceptedTags();
} else {
this._superTags = [];
}

// 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) {
if (this._superTags.includes(tag)) {
return this._superCollection.setTag(resource, tag, value);
}

this._validateResource(resource);
this._validateTag(tag);
this._validateValue(value);
Expand All @@ -25,6 +37,10 @@ class ResourceTagCollection {
}

clearTag(resource, tag) {
if (this._superTags.includes(tag)) {
return this._superCollection.clearTag(resource, tag);
}

this._validateResource(resource);
this._validateTag(tag);

Expand All @@ -35,6 +51,10 @@ class ResourceTagCollection {
}

getTag(resource, tag) {
if (this._superTags.includes(tag)) {
return this._superCollection.getTag(resource, tag);
}

this._validateResource(resource);
this._validateTag(tag);

Expand All @@ -44,6 +64,10 @@ class ResourceTagCollection {
}
}

getAcceptedTags() {
return [...this._allowedTags, ...this._superTags];
}

_validateResource(resource) {
const path = resource.getPath();
if (!path) {
Expand Down
66 changes: 66 additions & 0 deletions test/lib/ReaderFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const test = require("ava");
const sinon = require("sinon");
const ReaderFilter = require("../../lib/ReaderFilter");

test("_byGlob: Basic filter", async (t) => {
const abstractReader = {
_byGlob: sinon.stub().returns(Promise.resolve(["resource a", "resource b"]))
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
};
const trace = {
collection: sinon.spy()
};
const readerCollection = new ReaderFilter({
reader: abstractReader,
filterCallback: function(resource) {
if (resource === "resource a") {
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
return true;
}
});

const resources = await readerCollection._byGlob("anyPattern", {}, trace);
t.deepEqual(resources, ["resource b"], "Correct resource in result");
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
});

test("_byPath: Negative filter", async (t) => {
const abstractReader = {
_byPath: sinon.stub().returns(Promise.resolve("resource a"))
};
const trace = {
collection: sinon.spy()
};
const readerCollection = new ReaderFilter({
reader: abstractReader,
filterCallback: function(resource) {
if (resource === "resource a") {
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
return true;
}
});

const resources = await readerCollection._byPath("anyPattern", {}, trace);
t.deepEqual(resources, null, "Correct empty in result");
});

test("_byPath: Positive filter", async (t) => {
const abstractReader = {
_byPath: sinon.stub().returns(Promise.resolve("resource b"))
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
};
const trace = {
collection: sinon.spy()
};
const readerCollection = new ReaderFilter({
reader: abstractReader,
filterCallback: function(resource) {
if (resource === "resource a") {
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
return true;
}
});

const resources = await readerCollection._byPath("anyPattern", {}, trace);
t.deepEqual(resources, "resource b", "Correct resource in result");
RandomByte marked this conversation as resolved.
Show resolved Hide resolved
});
108 changes: 108 additions & 0 deletions test/lib/ResourceTagCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,114 @@ test("clearTag", (t) => {
"_validateTag called with correct arguments");
});

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

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

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

t.deepEqual(superTagCollection._pathTags, {
"/some/path": {
"abc:MySuperTag": "my super value"
}
}, "Super tag correctly stored");
t.deepEqual(tagCollection._pathTags, {
"/some/path": {
"abc:MyTag": "my value"
}
}, "Non-super 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:MySuperTag",
"_validateTag called with correct arguments");

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

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

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

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

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

t.is(value, 456, "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:MySuperTag",
"_validateTag called with correct arguments");
});

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

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

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

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

t.deepEqual(superTagCollection._pathTags, {
"/some/path": {
"abc:MySuperTag": 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:MySuperTag",
"_validateTag called with correct arguments");
});

test("_validateTag: Not in list of allowed tags", (t) => {
const tagCollection = new ResourceTagCollection({
allowedTags: ["abc:MyTag"]
Expand Down