Skip to content

Commit

Permalink
[FEATURE] Add Link-reader and WriterCollection
Browse files Browse the repository at this point in the history
Link reader:
Allows automatic rewriting of paths and glob patterns to other paths.
I.e. a path "/" can be rewritten to "/resorces/project/namespace"

WriterCollection:
Allows to define a collection of writers mapped to a set of path
prefixes.
I.e. one writer should be used when writing a resource with the path
prefix "/resources/project/namespace/*" and another one for all other
paths ("/")
  • Loading branch information
RandomByte committed Jun 13, 2022
1 parent adba34a commit a0e5cf3
Show file tree
Hide file tree
Showing 6 changed files with 501 additions and 6 deletions.
14 changes: 14 additions & 0 deletions lib/AbstractReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ class AbstractReader {
callback
});
}
/**
* Create a [Link-Reader]{@link module:@ui5/fs.readers.Link} from the current reader
*
* @public
* @param {module:@ui5/fs.reader.Link.PathMapping} pathMapping Link configuration
* @returns {module:@ui5/fs.reader.Link} Link instance
*/
link(pathMapping) {
const Link = require("./readers/Link");
return new Link({
reader: this,
pathMapping
});
}

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

/**
* Resource Locator WriterCollection
*
* @public
* @memberof module:@ui5/fs
* @augments module:@ui5/fs.AbstractReaderWriter
*/
class WriterCollection extends AbstractReaderWriter {
/**
* The constructor.
*
* @param {object} parameters Parameters
* @param {object[]<string,module:@ui5/fs.AbstractReaderWriter[]>} parameters.writerMapping
* Mapping of virtual base paths to writers. Path are matched greedy
* @param {string} parameters.name The collection name
*/
constructor({writerMapping, name}) {
super();
this._name = name;

if (!writerMapping) {
throw new Error("Missing parameter 'writerMapping'");
}
const basePaths = Object.keys(writerMapping);
if (!basePaths.length) {
throw new Error("Empty parameter 'writerMapping'");
}

// Create a regular expression (which is greedy by nature) from all paths to easily
// find the correct writer for any given resource path
this._basePathRegex = basePaths.sort().reduce((regex, basePath, idx) => {
// Validate base path
if (!basePath) {
throw new Error(`Empty path in path mapping of WriterCollection ${this._name}`);
}
if (!basePath.startsWith("/")) {
throw new Error(
`Missing leading slash in path mapping '${basePath}' of WriterCollection ${this._name}`);
}
if (!basePath.endsWith("/")) {
throw new Error(
`Missing trailing slash in path mapping '${basePath}' of WriterCollection ${this._name}`);
}

return `${regex}(?:${basePath.replace(/\//g, "\\/")})??`;
}, "^(") + ")+.*?$";

this._writerMapping = writerMapping;
this._readerCollection = new ReaderCollection({
name: `Reader collection of writer collection '${this._name}'`,
readers: Object.values(writerMapping)
});
}

/**
* 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
*/
_byGlob(pattern, options, trace) {
return this._readerCollection._byGlob(pattern, options, trace);
}

/**
* 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
*/
_byPath(virPath, options, trace) {
return this._readerCollection._byPath(virPath, options, trace);
}

/**
* Writes the content of a resource to a path.
*
* @private
* @param {module:@ui5/fs.Resource} resource The Resource to write
* @param {object} [options] Write options, see above
* @returns {Promise<undefined>} Promise resolving once data has been written
*/
_write(resource, options) {
const resourcePath = resource.getPath();

const basePathMatch = resourcePath.match(this._basePathRegex);
if (!basePathMatch || basePathMatch.length < 2) {
throw new Error(
`Failed to find a writer for resource with path ${resourcePath} in WriterCollection ${this._name}. ` +
`Base paths handled by this collection are: ${Object.keys(this._writerMapping).join(", ")}`);
}
const writer = this._writerMapping[basePathMatch[1]];
return writer._write(resource, options);
}

_validateBasePath(writerMapping) {
Object.keys(writerMapping).forEach((path) => {
if (!path) {
throw new Error(`Empty path in path mapping of WriterCollection ${this._name}`);
}
if (!path.startsWith("/")) {
throw new Error(`Missing leading slash in path mapping '${path}' of WriterCollection ${this._name}`);
}
if (!path.endsWith("/")) {
throw new Error(`Missing trailing slash in path mapping '${path}' of WriterCollection ${this._name}`);
}
});
}
}

module.exports = WriterCollection;
122 changes: 122 additions & 0 deletions lib/readers/Link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const AbstractReader = require("../AbstractReader");
const ResourceFacade = require("../ResourceFacade");
const resourceFactory = require("../resourceFactory");
const log = require("@ui5/logger").getLogger("resources:readers:Link");

/**
* A reader that allows modification of all resources passed through it.
*
* @public
* @memberof module:@ui5/fs.readers
* @augments module:@ui5/fs.AbstractReader
*/
class Link extends AbstractReader {
/**
* Path mapping for a [Link]{@link module:@ui5/fs.readers.Link}
*
* @public
* @typedef {object} PathMapping
* @property {string} pathMapping.linkPath Input path to rewrite
* @property {string} pathMapping.targetPath Path to rewrite to
*/
/**
* Constructor
*
* @public
* @param {object} parameters Parameters
* @param {module:@ui5/fs.AbstractReader} parameters.reader The resource reader to wrap
* @param {PathMapping} parameters.pathMapping
*/
constructor({reader, pathMapping}) {
super();
if (!reader) {
throw new Error(`Missing parameter "reader"`);
}
if (!pathMapping) {
throw new Error(`Missing parameter "pathMapping"`);
}
this._reader = reader;
this._pathMapping = pathMapping;
this._validatePathMapping(pathMapping);
}

/**
* Locates resources by glob.
*
* @private
* @param {string|string[]} patterns 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(patterns, options, trace) {
if (!(patterns instanceof Array)) {
patterns = [patterns];
}
patterns = patterns.map((pattern) => {
if (pattern.startsWith(this._pathMapping.linkPath)) {
pattern = pattern.substr(this._pathMapping.linkPath.length);
}
return resourceFactory.prefixGlobPattern(pattern, this._pathMapping.targetPath);
});

// Flatten prefixed patterns
patterns = Array.prototype.concat.apply([], patterns);

// Keep resource's internal path unchanged for now
const resources = await this._reader._byGlob(patterns, options, trace);
return resources.map((resource) => {
const resourcePath = resource.getPath();
if (resourcePath.startsWith(this._pathMapping.targetPath)) {
return new ResourceFacade({
resource,
path: this._pathMapping.linkPath + resourcePath.substr(this._pathMapping.targetPath.length)
});
}
});
}

/**
* 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) {
if (!virPath.startsWith(this._pathMapping.linkPath)) {
return null;
}
const targetPath = this._pathMapping.targetPath + virPath.substr(this._pathMapping.linkPath.length);
log.verbose(`byPath: Rewriting virtual path ${virPath} to ${targetPath}`);

const resource = await this._reader._byPath(targetPath, options, trace);
if (resource) {
return new ResourceFacade({
resource,
path: this._pathMapping.linkPath + resource.getPath().substr(this._pathMapping.targetPath.length)
});
}
return null;
}

_validatePathMapping({linkPath, targetPath}) {
if (!linkPath) {
throw new Error(`Path mapping is missing attribute "linkPath"`);
}
if (!targetPath) {
throw new Error(`Path mapping is missing attribute "targetPath"`);
}
if (!linkPath.endsWith("/")) {
throw new Error(`Link path must end with a slash: ${linkPath}`);
}
if (!targetPath.endsWith("/")) {
throw new Error(`Target path must end with a slash: ${targetPath}`);
}
}
}

module.exports = Link;
42 changes: 42 additions & 0 deletions lib/resourceFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,29 @@ const resourceFactory = {
},

createReaderCollection({name, readers}) {
const ReaderCollection = require("./ReaderCollection");
return new ReaderCollection({
name,
readers
});
},

createReaderCollectionPrioritized({name, readers}) {
const ReaderCollectionPrioritized = require("./ReaderCollectionPrioritized");
return new ReaderCollectionPrioritized({
name,
readers
});
},

createWriterCollection({name, writerMapping}) {
const WriterCollection = require("./WriterCollection");
return new WriterCollection({
name,
writerMapping
});
},

/**
* Creates a <code>Resource</code>. Accepts the same parameters as the Resource constructor.
*
Expand Down Expand Up @@ -313,6 +330,31 @@ const resourceFactory = {
writer,
name
});
},

/**
* Normalizes virtual glob patterns by prefixing them with
* a given virtual base directory path
*
* @param {string} virPattern glob pattern for virtual directory structure
* @param {string} virBaseDir virtual base directory path to prefix the given patterns with
* @returns {string[]} A list of normalized glob patterns
*/
prefixGlobPattern(virPattern, virBaseDir) {
const path = require("path");
const minimatch = require("minimatch");
const mm = new minimatch.Minimatch(virPattern);

const resultGlobs = [];
for (let i = 0; i < mm.globSet.length; i++) {
let resultPattern = path.posix.join(virBaseDir, mm.globSet[i]);

if (mm.negate) {
resultPattern = "!" + resultPattern;
}
resultGlobs.push(resultPattern);
}
return Array.prototype.concat.apply([], resultGlobs);
}
};

Expand Down
Loading

0 comments on commit a0e5cf3

Please sign in to comment.