forked from tools/josh
1
0
Fork 0

Distinguish file and directory paths

Fixes #76.
This commit is contained in:
Florine W. Dekker 2019-11-13 14:47:06 +01:00
parent ee7b7b3663
commit 6c10b714cd
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
5 changed files with 177 additions and 77 deletions

View File

@ -1,6 +1,6 @@
{
"name": "fwdekker.com",
"version": "1.13.0",
"version": "1.14.0",
"description": "The source code of [my personal website](https://fwdekker.com/).",
"author": "Felix W. Dekker",
"repository": {

View File

@ -62,6 +62,9 @@ export class FileSystem {
* or if there already exists a node at the given location
*/
add(target: Path, node: Node, createParents: boolean): void {
if (target.isDirectory && !(node instanceof Directory))
throw new IllegalArgumentError(`Cannot add non-directory at '${target}/'.`);
if (!this.has(target.parent)) {
if (createParents)
this.add(target.parent, new Directory(), true);
@ -72,10 +75,10 @@ export class FileSystem {
const parent = this.get(target.parent);
if (!(parent instanceof Directory))
throw new IllegalArgumentError(`'${target.parent}' is not a directory.`);
if (parent.hasNode(target.fileName))
if (parent.has(target.fileName))
throw new IllegalArgumentError(`A file or directory already exists at '${target}'.`);
parent.addNode(target.fileName, node);
parent.add(target.fileName, node);
}
/**
@ -110,10 +113,14 @@ export class FileSystem {
return this.root;
const parent = this.get(target.parent);
if (!(parent instanceof Directory) || !parent.hasNode(target.fileName))
if (!(parent instanceof Directory))
return undefined;
return parent.getNode(target.fileName);
const node = parent.get(target.fileName);
if (target.isDirectory && !(node instanceof Directory))
return undefined;
return parent.get(target.fileName);
}
/**
@ -122,14 +129,7 @@ export class FileSystem {
* @param target the path to check for node presence
*/
has(target: Path): boolean {
if (target.toString() === "/")
return true;
const parent = this.get(target.parent);
if (!(parent instanceof Directory))
return false;
return parent.hasNode(target.fileName);
return this.get(target) !== undefined;
}
/**
@ -161,14 +161,18 @@ export class FileSystem {
*/
open(target: Path, mode: FileMode): FileStream {
if (!this.has(target.parent))
throw new IllegalArgumentError(`open: Directory '${target.parent}' does not exist.`);
throw new IllegalArgumentError(`Directory '${target.parent}' does not exist.`);
if (!this.has(target))
this.add(target, new File(), false);
if (!this.has(target)) {
if (mode === "append" || mode === "write")
this.add(target, new File(), false);
else
throw new IllegalArgumentError(`File '${target}' does not exist.`);
}
const targetNode = this.get(target);
if (!(targetNode instanceof File))
throw new IllegalArgumentError(`open: Cannot open stream to directory '${target}'.`);
throw new IllegalArgumentError(`Cannot open directory '${target}'.`);
return targetNode.open(mode);
}
@ -178,14 +182,18 @@ export class FileSystem {
*
* If the node in question does not exist, the function will return successfully.
*
* @param targetPath the path to the node to be removed
* @param target the path to the node to be removed
*/
remove(targetPath: Path): void {
const parent = this.get(targetPath.parent);
remove(target: Path): void {
const parent = this.get(target.parent);
if (!(parent instanceof Directory))
return;
parent.removeNode(targetPath.fileName);
const node = this.get(target);
if (target.isDirectory && !(node instanceof Directory))
return;
parent.remove(target.fileName);
}
}
@ -206,12 +214,16 @@ export class Path {
* The name of the node described by this path.
*/
readonly fileName: string;
/**
* `true` if and only if the path necessarily points to a directory.
*/
readonly isDirectory: boolean;
/**
* Constructs a new path.
*
* @param paths a string that describes the path
* @param paths a set of strings that describe the path
*/
constructor(...paths: string[]) {
const path = `/${paths.join("/")}/`;
@ -240,6 +252,7 @@ export class Path {
this.path = "/" + parts.join("/");
this._parent = parts.slice(0, -1).join("/");
this.fileName = parts.slice(-1).join("");
this.isDirectory = paths[paths.length - 1].endsWith("/");
}
/**
@ -447,15 +460,11 @@ export class Directory extends Node {
/**
* Returns the node with the given name.
* Returns the node with the given name, or `undefined` if there is no such node.
*
* @param name the name of the node to return
* @throws when there is no node with the given name in this directory
*/
getNode(name: string): Node {
if (!this.hasNode(name))
throw new IllegalArgumentError(`Directory does not have a node with name '${name}'.`);
get(name: string): Node | undefined {
return this._nodes[name];
}
@ -464,7 +473,7 @@ export class Directory extends Node {
*
* @param name the name to check
*/
hasNode(name: string): boolean {
has(name: string): boolean {
return this._nodes.hasOwnProperty(name);
}
@ -474,7 +483,7 @@ export class Directory extends Node {
* @param name the name of the node in this directory
* @param node the node to add to this directory
*/
addNode(name: string, node: Node): void {
add(name: string, node: Node): void {
if (new Path(`/${name}`).toString() === "/" || name.indexOf("/") >= 0)
throw new IllegalArgumentError(`Cannot add node with name '${name}'.`);
@ -487,9 +496,9 @@ export class Directory extends Node {
* @param name the name of the node to remove
* @throws if the given node is not contained in this directory
*/
removeNode(name: string): void {
remove(name: string): void {
if (name === "" || name === ".") {
Object.keys(this._nodes).forEach(node => this.removeNode(node));
Object.keys(this._nodes).forEach(node => this.remove(node));
return;
}

View File

@ -20,103 +20,103 @@ describe("directory", () => {
});
it("has the given nodes", () => {
expect(new Directory({node: new File()}).hasNode("node")).to.be.true;
expect(new Directory({node: new File()}).has("node")).to.be.true;
});
});
describe("getNode", () => {
describe("get", () => {
it("throws an exception if he node does not exist", () => {
expect(() => directory.getNode("error")).to.throw();
expect(directory.get("error")).to.be.undefined;
});
it("does not contain itself", () => {
expect(() => directory.getNode(".")).to.throw();
expect(directory.get(".")).to.be.undefined;
});
it("returns the desired node", () => {
const file = new File();
directory.addNode("file", file);
directory.add("file", file);
expect(directory.getNode("file")).to.equal(file);
expect(directory.get("file")).to.equal(file);
});
});
describe("hasNode", () => {
describe("has", () => {
it("returns false if the node does not exist", () => {
expect(directory.hasNode("error")).to.be.false;
expect(directory.has("error")).to.be.false;
});
it("returns true if the node exists", () => {
directory.addNode("file", new File());
directory.add("file", new File());
expect(directory.hasNode("file")).to.be.true;
expect(directory.has("file")).to.be.true;
});
it("returns false if the node is the reflexive node", () => {
expect(directory.hasNode(".")).to.be.false;
expect(directory.has(".")).to.be.false;
});
it("returns false if the node is the parent node", () => {
expect(directory.hasNode("..")).to.be.false;
expect(directory.has("..")).to.be.false;
});
it("returns false if the node refers to itself", () => {
expect(directory.hasNode("")).to.be.false;
expect(directory.has("")).to.be.false;
});
});
describe("addNode", () => {
describe("add", () => {
it("adds the given node", () => {
directory.addNode("file", new File());
directory.add("file", new File());
expect(directory.hasNode("file")).to.be.true;
expect(directory.has("file")).to.be.true;
});
it("overwrites an existing node", () => {
directory.addNode("file", new File());
directory.addNode("file", new Directory());
directory.add("file", new File());
directory.add("file", new Directory());
expect(directory.getNode("file")).to.be.instanceOf(Directory);
expect(directory.get("file")).to.be.instanceOf(Directory);
});
it("refuses to add a node at the reflexive path", () => {
expect(() => directory.addNode(".", new File())).to.throw();
expect(() => directory.add(".", new File())).to.throw();
});
it("refuses to add a node at the parent path", () => {
expect(() => directory.addNode("..", new File())).to.throw();
expect(() => directory.add("..", new File())).to.throw();
});
it("refuses to add a node that refers to the directory", () => {
expect(() => directory.addNode("", new File())).to.throw();
expect(() => directory.add("", new File())).to.throw();
});
it("refuses to add a node with a name containing a slash", () => {
expect(() => directory.addNode("a/b", new File())).to.throw();
expect(() => directory.add("a/b", new File())).to.throw();
});
});
describe("removeNode", () => {
describe("remove", () => {
it("removes the desired node", () => {
directory.addNode("file", new File());
directory.add("file", new File());
directory.removeNode("file");
directory.remove("file");
expect(directory.nodeCount).to.equal(0);
});
it("empties the directory if the name is empty", () => {
directory.addNode("file", new File());
directory.add("file", new File());
directory.removeNode("");
directory.remove("");
expect(directory.nodeCount).to.equal(0);
});
it("empties the directory if the name is reflexive", () => {
directory.addNode("file", new File());
directory.add("file", new File());
directory.removeNode(".");
directory.remove(".");
expect(directory.nodeCount).to.equal(0);
});
@ -130,13 +130,13 @@ describe("directory", () => {
});
const copy = directory.copy();
(<File>directory.getNode("file")).open("write").write("changed");
expect((<File>copy.getNode("file")).open("read").read()).to.equal("contents");
(<File>directory.get("file")).open("write").write("changed");
expect((<File>copy.get("file")).open("read").read()).to.equal("contents");
(<Directory>directory.getNode("dir")).addNode("file2", new File());
expect((<Directory>copy.getNode("dir")).nodeCount).to.equal(0);
(<Directory>directory.get("dir")).add("file2", new File());
expect((<Directory>copy.get("dir")).nodeCount).to.equal(0);
directory.removeNode("file");
directory.remove("file");
expect(copy.nodeCount).to.equal(2);
});
});

View File

@ -62,6 +62,10 @@ describe("file system", () => {
expect(() => fileSystem.add(new Path("/file1"), new File(), false)).to.throw();
});
it("fails if a file is added at a directory path", () => {
expect(() => fileSystem.add(new Path("/dir/"), new File(), false)).to.throw();
});
});
describe("copy", () => {
@ -132,6 +136,42 @@ describe("file system", () => {
expect(fileSystem.get(new Path("/"))).to.equal(fileSystem.root);
});
it("returns the node at the given path", () => {
const file = new File();
fileSystem.add(new Path("/dir"), new Directory(), false);
fileSystem.add(new Path("/dir/file"), file, false);
expect(fileSystem.get(new Path("/dir/file"))).to.equal(file);
});
it("returns the file at the given file path", () => {
const file = new File();
fileSystem.add(new Path("/file"), file, false);
expect(fileSystem.get(new Path("/file"))).to.equal(file);
});
it("returns the directory at the given file path", () => {
const directory = new Directory();
fileSystem.add(new Path("/dir"), directory, false);
expect(fileSystem.get(new Path("/dir"))).to.equal(directory);
});
it("returns undefined for a directory path even though a file exists with the same name", () => {
const file = new File();
fileSystem.add(new Path("/file"), file, false);
expect(fileSystem.get(new Path("/file/"))).to.be.undefined;
});
it("returns the directory at the given directory path", () => {
const directory = new Directory();
fileSystem.add(new Path("/dir"), directory, false);
expect(fileSystem.get(new Path("/dir/"))).to.equal(directory);
});
it("returns undefined if the parent is not a directory", () => {
fileSystem.add(new Path("/file1"), new File(), false);
@ -143,14 +183,6 @@ describe("file system", () => {
expect(fileSystem.get(new Path("/dir/file"))).to.be.undefined;
});
it("returns the node at the given path", () => {
const file = new File();
fileSystem.add(new Path("/dir"), new Directory(), false);
fileSystem.add(new Path("/dir/file"), file, false);
expect(fileSystem.get(new Path("/dir/file"))).to.equal(file);
});
});
describe("has", () => {
@ -158,6 +190,30 @@ describe("file system", () => {
expect(fileSystem.has(new Path("/"))).to.be.true;
});
it("returns true if a file exists at the given file path", () => {
fileSystem.add(new Path("/dir"), new File(), false);
expect(fileSystem.has(new Path("/dir"))).to.be.true;
});
it("returns true if a directory exists at the given file path", () =>{
fileSystem.add(new Path("/dir"), new Directory(), false);
expect(fileSystem.has(new Path("/dir"))).to.be.true;
});
it("returns false if a file with the same name exists but a directory path is given", () => {
fileSystem.add(new Path("/dir"), new File(), false);
expect(fileSystem.has(new Path("/dir/"))).to.be.false;
});
it("returns true if a directory exists at the given directory path", () => {
fileSystem.add(new Path("/dir"), new Directory(), false);
expect(fileSystem.has(new Path("/dir/"))).to.be.true;
});
it("returns false if the node does not exist", () => {
expect(fileSystem.has(new Path("/error"))).to.be.false;
});
@ -242,8 +298,12 @@ describe("file system", () => {
expect(() => fileSystem.open(new Path("/dir"), "read")).to.throw();
});
it("creates the target if it does not exist yet", () => {
fileSystem.open(new Path("/file"), "read");
it("throws an error in read mode if a directory path is given", () => {
expect(() => fileSystem.open(new Path("/dir/"), "read")).to.throw();
});
it("creates the target in write mode if it does not exist yet", () => {
fileSystem.open(new Path("/file"), "write");
expect(fileSystem.has(new Path("/file"))).to.be.true;
});
@ -258,6 +318,7 @@ describe("file system", () => {
describe("remove", () => {
it("removes a file", () => {
fileSystem.add(new Path("/file"), new File(), false);
fileSystem.remove(new Path("/file"));
expect(fileSystem.has(new Path("/file"))).to.be.false;
@ -282,5 +343,13 @@ describe("file system", () => {
expect(fileSystem.has(new Path("/dir"))).to.be.false;
});
it("does not remove a file at a directory path", () => {
fileSystem.add(new Path("/file"), new File(), false);
fileSystem.remove(new Path("/file/"));
expect(fileSystem.has(new Path("/file"))).to.be.true;
});
});
});

View File

@ -48,6 +48,10 @@ describe("paths", () => {
});
describe("parts", () => {
it("throws an error if no parts are given", () => {
expect(() => new Path()).to.throw();
});
it("concatenates multiple parts", () => {
expect(new Path("/dir1", "/dir2", "/file").toString()).to.equal("/dir1/dir2/file");
});
@ -94,6 +98,24 @@ describe("paths", () => {
});
});
describe("directory", () => {
it("is a directory path if the last part ends with a slash", () => {
expect(new Path("/dir/").isDirectory).to.be.true;
});
it("is a directory if the last part ends with a slash", () => {
expect(new Path("/dir1", "dir2/").isDirectory).to.be.true;
});
it("is not a directory path if the last part does not end with a slash", () => {
expect(new Path("/dir").isDirectory).to.be.false;
});
it("is not a directory if only the first part ends with a slash", () => {
expect(new Path("/dir/", "file").isDirectory).to.be.false;
});
});
describe("parent", () => {
it("returns root as the parent of root", () => {
expect(new Path("/").parent.toString()).to.equal(new Path("/").toString());