Skip to content

Commit

Permalink
[INTERNAL] Resource: Always set internal sourceMetadata attribute
Browse files Browse the repository at this point in the history
This ensures that the contentModified flag is tracked for all
resources, not only for those created by a FileSystem adapter.
  • Loading branch information
RandomByte committed Oct 5, 2023
1 parent 16b1cd9 commit 181803a
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 26 deletions.
52 changes: 34 additions & 18 deletions lib/Resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import posixPath from "node:path/posix";

const fnTrue = () => true;
const fnFalse = () => false;
const ALLOWED_SOURCE_METADATA_KEYS = ["adapter", "fsPath", "contentModified"];

/**
* Resource. UI5 Tooling specific representation of a file's content and metadata
Expand Down Expand Up @@ -54,7 +55,8 @@ class Resource {
* In some cases this is the most memory-efficient way to supply resource content
* @param {@ui5/project/specifications/Project} [parameters.project] Project this resource is associated with
* @param {object} [parameters.sourceMetadata] Source metadata for UI5 Tooling internal use.
* Typically set by an adapter to store information for later retrieval.
* Some information may be set by an adapter to store information for later retrieval. Also keeps track of whether
* a resource content has been modified since it has been read from a source
*/
constructor({path, statInfo, buffer, string, createStream, stream, project, sourceMetadata}) {
if (!path) {
Expand All @@ -66,16 +68,34 @@ class Resource {
"'buffer', 'string', 'stream' or 'createStream'");
}

if (sourceMetadata) {
if (typeof sourceMetadata !== "object") {
throw new Error(`Parameter 'sourceMetadata' must be of type "object"`);
}

/* eslint-disable-next-line guard-for-in */
for (const metadataKey in sourceMetadata) { // Also check prototype
if (!ALLOWED_SOURCE_METADATA_KEYS.includes(metadataKey)) {
throw new Error(`Parameter 'sourceMetadata' contains an illegal attribute: ${metadataKey}`);
}
if (!["string", "boolean"].includes(typeof sourceMetadata[metadataKey])) {
throw new Error(
`Attribute '${metadataKey}' of parameter 'sourceMetadata' ` +
`must be of type "string" or "boolean"`);
}
}
}

this.setPath(path);

this.#sourceMetadata = sourceMetadata;
if (this.#sourceMetadata) {
// This flag indicates whether a resource has changed from its original source.
// resource.isModified() is not sufficient, since it only reflects the modification state of the
// current instance.
// Since the sourceMetadata object is inherited to clones, it is the only correct indicator
this.#sourceMetadata.contentModified = this.#sourceMetadata.contentModified || false;
}
this.#sourceMetadata = sourceMetadata || {};

// This flag indicates whether a resource has changed from its original source.
// resource.isModified() is not sufficient, since it only reflects the modification state of the
// current instance.
// Since the sourceMetadata object is inherited to clones, it is the only correct indicator
this.#sourceMetadata.contentModified ??= false;

this.#isModified = false;

this.#project = project;
Expand Down Expand Up @@ -142,9 +162,7 @@ class Resource {
* @param {Buffer} buffer Buffer instance
*/
setBuffer(buffer) {
if (this.#sourceMetadata) {
this.#sourceMetadata.contentModified = true;
}
this.#sourceMetadata.contentModified = true;
this.#isModified = true;
this.#setBuffer(buffer);
}
Expand Down Expand Up @@ -234,9 +252,7 @@ class Resource {
*/
setStream(stream) {
this.#isModified = true;
if (this.#sourceMetadata) {
this.#sourceMetadata.contentModified = true;
}
this.#sourceMetadata.contentModified = true;

this.#buffer = null;
// if (this.#stream) { // TODO this may cause strange issues
Expand Down Expand Up @@ -424,13 +440,13 @@ class Resource {
}

/**
* Returns source metadata if any where provided during the creation of this resource.
* Returns source metadata which may contain information specific to the adapter that created the resource
* Typically set by an adapter to store information for later retrieval.
*
* @returns {object|null}
* @returns {object}
*/
getSourceMetadata() {
return this.#sourceMetadata || null;
return this.#sourceMetadata;
}

/**
Expand Down
77 changes: 69 additions & 8 deletions test/lib/Resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ test("Resource: constructor with duplicated content parameter", (t) => {
test("Resource: From buffer", async (t) => {
const resource = new Resource({
path: "/my/path",
buffer: Buffer.from("Content"),
sourceMetadata: {}
buffer: Buffer.from("Content")
});
t.is(await resource.getSize(), 7, "Content is set");
t.false(resource.isModified(), "Content of new resource is not modified");
Expand All @@ -105,8 +104,7 @@ test("Resource: From buffer", async (t) => {
test("Resource: From string", async (t) => {
const resource = new Resource({
path: "/my/path",
string: "Content",
sourceMetadata: {}
string: "Content"
});
t.is(await resource.getSize(), 7, "Content is set");
t.false(resource.isModified(), "Content of new resource is not modified");
Expand All @@ -117,8 +115,7 @@ test("Resource: From stream", async (t) => {
const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html");
const resource = new Resource({
path: "/my/path",
stream: createReadStream(fsPath),
sourceMetadata: {}
stream: createReadStream(fsPath)
});
t.is(await resource.getSize(), 91, "Content is set");
t.false(resource.isModified(), "Content of new resource is not modified");
Expand All @@ -131,14 +128,78 @@ test("Resource: From createStream", async (t) => {
path: "/my/path",
createStream: () => {
return createReadStream(fsPath);
},
sourceMetadata: {}
}
});
t.is(await resource.getSize(), 91, "Content is set");
t.false(resource.isModified(), "Content of new resource is not modified");
t.false(resource.getSourceMetadata().contentModified, "Content of new resource is not modified");
});

test("Resource: Source metadata", async (t) => {
const resource = new Resource({
path: "/my/path",
string: "Content",
sourceMetadata: {
adapter: "My Adapter",
fsPath: "/some/path"
}
});
t.is(await resource.getSize(), 7, "Content is set");
t.false(resource.isModified(), "Content of new resource is not modified");
t.false(resource.getSourceMetadata().contentModified, "Content of new resource is not modified");
t.is(resource.getSourceMetadata().adapter, "My Adapter", "Correct source metadata 'adapter' value");
t.is(resource.getSourceMetadata().fsPath, "/some/path", "Correct source metadata 'fsPath' value");
});
test("Resource: Source metadata with modified content", async (t) => {
const resource = new Resource({
path: "/my/path",
string: "Content",
sourceMetadata: {
adapter: "My Adapter",
fsPath: "/some/path",
contentModified: true
}
});
t.is(await resource.getSize(), 7, "Content is set");
t.false(resource.isModified(), "Content of new resource is not modified");
t.true(resource.getSourceMetadata().contentModified, "Content of new resource is already modified");
t.is(resource.getSourceMetadata().adapter, "My Adapter", "Correct source metadata 'adapter' value");
t.is(resource.getSourceMetadata().fsPath, "/some/path", "Correct source metadata 'fsPath' value");
});

test("Resource: Illegal source metadata attribute", (t) => {
t.throws(() => {
new Resource({
path: "/my/path",
string: "Content",
sourceMetadata: {
adapter: "My Adapter",
fsPath: "/some/path",
pony: "🦄"
}
});
}, {
message: `Parameter 'sourceMetadata' contains an illegal attribute: pony`
}, "Threw with expected error message");
});

test("Resource: Illegal source metadata value", (t) => {
t.throws(() => {
new Resource({
path: "/my/path",
string: "Content",
sourceMetadata: {
adapter: "My Adapter",
fsPath: {
some: "value"
}
}
});
}, {
message: `Attribute 'fsPath' of parameter 'sourceMetadata' must be of type "string" or "boolean"`
}, "Threw with expected error message");
});

test("Resource: getBuffer with throwing an error", (t) => {
t.plan(1);

Expand Down

0 comments on commit 181803a

Please sign in to comment.