Port AbsolutePath implementation to JS (#2325)

This commit is contained in:
Ben Kalman
2016-08-10 13:44:08 -07:00
committed by GitHub
parent 33121dc102
commit 630d3a29cc
8 changed files with 228 additions and 4 deletions

View File

@@ -0,0 +1,100 @@
// @flow
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import {assert} from 'chai';
import {suite, test} from 'mocha';
import {equals} from './compare.js';
import {invariant, notNull} from './assert.js';
import AbsolutePath from './absolute-path.js';
import Commit from './commit.js';
import {getHash} from './get-hash.js';
import {stringLength} from './hash.js';
import List from './list.js';
import Set from './set.js';
import {TestDatabase} from './test-util.js';
import type Value from './value.js';
suite('AbsolutePath', () => {
test('to and from string', () => {
const t = (str: string) => {
const p = AbsolutePath.parse(str);
assert.strictEqual(str, p.toString());
};
const h = getHash(42); // arbitrary hash
t(`foo.bar[#${h.toString()}]`);
t(`#${h.toString()}.bar[42]`);
});
test('absolute paths', async () => {
const s0 = 'foo', s1 = 'bar';
const list = new List([s0, s1]);
const emptySet = new Set();
let db = new TestDatabase();
db.writeValue(s0);
db.writeValue(s1);
db.writeValue(list);
db.writeValue(emptySet);
db = await db.commit('ds', new Commit(list));
const head = await db.head('ds');
invariant(head);
const resolvesTo = async (exp: Value | null, str: string) => {
const p = AbsolutePath.parse(str);
const act = await notNull(p).resolve(db);
if (exp === null) {
assert.strictEqual(null, act);
} else if (act === null) {
assert.isTrue(false, `Failed to resolve ${str}`);
} else {
assert.isTrue(equals(exp, act));
}
};
await resolvesTo(head, 'ds');
await resolvesTo(emptySet, 'ds.parents');
await resolvesTo(list, 'ds.value');
await resolvesTo(s0, 'ds.value[0]');
await resolvesTo(s1, 'ds.value[1]');
await resolvesTo(head, '#' + getHash(head).toString());
await resolvesTo(list, '#' + getHash(list).toString());
await resolvesTo(s0, `#${getHash(s0).toString()}`);
await resolvesTo(s1, `#${getHash(s1).toString()}`);
await resolvesTo(s0, `#${getHash(list).toString()}[0]`);
await resolvesTo(s1, `#${getHash(list).toString()}[1]`);
await resolvesTo(null, 'foo');
await resolvesTo(null, 'foo.parents');
await resolvesTo(null, 'foo.value');
await resolvesTo(null, 'foo.value[0]');
await resolvesTo(null, `#${getHash('baz').toString()}`);
await resolvesTo(null, `#${getHash('baz').toString()}[0]`);
});
test('parse errors', () => {
const t = (path: string, exp: string) => {
let act = '';
try {
AbsolutePath.parse(path);
} catch (e) {
assert.instanceOf(e, SyntaxError);
act = e.message;
}
assert.strictEqual(exp, act);
};
t('', 'Empty path');
t('.foo', 'Invalid dataset name: .foo');
t('.foo.bar.baz', 'Invalid dataset name: .foo.bar.baz');
t('#', 'Invalid hash: ');
t('#abc', 'Invalid hash: abc');
const invalidHash = new Array(stringLength).join('z');
t(`#${invalidHash}`, `Invalid hash: ${invalidHash}`);
});
});

107
js/src/absolute-path.js Normal file
View File

@@ -0,0 +1,107 @@
// @flow
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import {invariant} from './assert.js';
import {datasetRe} from './dataset.js';
import Database from './database.js';
import Hash, {stringLength} from './hash.js';
import Path from './path.js';
import type Value from './value.js';
const datasetCapturePrefixRe = new RegExp('^(' + datasetRe.source + ')');
/**
* An AbsolutePath is a Path relative to either a dataset head, or a hash.
*
* E.g. in a spec like `http://demo.noms.io::foo.bar` this is the `foo.bar` component, or in
* `http://demo.noms.io::#abcdef.bar` the `#abcdef.bar` component.
*/
export default class AbsolutePath {
/** The dataset ID that `path` is in, or `''` if none. */
dataset: string;
/** The hash the that `path` is in, if any. */
hash: Hash | null;
/** Path relative to either `dataset` or `hash`. */
path: Path;
/**
* Returns `str` parsed as an AbsolutePath. Throws a `SyntaxError` if `str` isn't a valid path.
*/
static parse(str: string): AbsolutePath {
if (str === '') {
throw new SyntaxError('Empty path');
}
let dataset = '';
let hash = null;
let pathStr = '';
if (str[0] === '#') {
const tail = str.slice(1);
if (tail.length < stringLength) {
throw new SyntaxError(`Invalid hash: ${tail}`);
}
const hashStr = tail.slice(0, stringLength);
hash = Hash.parse(hashStr);
if (hash === null) {
throw new SyntaxError(`Invalid hash: ${hashStr}`);
}
pathStr = tail.slice(stringLength);
} else {
const parts = datasetCapturePrefixRe.exec(str);
if (!parts) {
throw new SyntaxError(`Invalid dataset name: ${str}`);
}
invariant(parts.length === 2);
dataset = parts[1];
pathStr = str.slice(parts[0].length);
}
if (pathStr.length === 0) {
return new AbsolutePath(dataset, hash, new Path());
}
const path = Path.parse(pathStr);
return new AbsolutePath(dataset, hash, path);
}
constructor(dataset: string, hash: Hash | null, path: Path) {
this.dataset = dataset;
this.hash = hash;
this.path = path;
}
async resolve(db: Database): Promise<Value | null> {
let val = null;
if (this.dataset !== '') {
val = await db.head(this.dataset);
} else if (this.hash !== null) {
val = await db.readValue(this.hash);
} else {
throw new Error('unreachable');
}
if (val === undefined) {
val = null;
}
return val === null ? null : this.path.resolve(val);
}
toString(): string {
if (this.dataset !== '') {
return this.dataset + this.path.toString();
}
if (this.hash !== null) {
return '#' + this.hash.toString() + this.path.toString();
}
throw new Error('unreachable');
}
}

View File

@@ -44,10 +44,12 @@ export default class Database {
});
}
// TODO: This should return Promise<Ref<Commit> | null>.
headRef(datasetID: string): Promise<?Ref<Commit>> {
return this._datasets.then(datasets => datasets.get(datasetID));
}
// TODO: This should return Promise<Commit | null>
head(datasetID: string): Promise<?Commit> {
return this.headRef(datasetID).then(hr => hr ? this.readValue(hr.targetHash) : null);
}
@@ -56,7 +58,7 @@ export default class Database {
return this._datasets;
}
// TODO: This should return Promise<?Value>
// TODO: This should return Promise<Value | null>
async readValue(hash: Hash): Promise<any> {
return this._vs.readValue(hash);
}

View File

@@ -10,7 +10,11 @@ import type Database from './database.js';
import Ref from './ref.js';
import Set from './set.js';
const idRe = /^[a-zA-Z0-9\-_/]+$/;
/** Matches any valid dataset name in a string. */
export const datasetRe = /^[a-zA-Z0-9\-_/]+/;
/** Matches if an entire string is a valid dataset name. */
const idRe = new RegExp('^' + datasetRe.source + '$');
export default class Dataset {
_database: Database;

View File

@@ -4,6 +4,7 @@
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
export {default as AbsolutePath} from './absolute-path.js';
export {AsyncIterator} from './async-iterator.js';
export {default as BuzHash} from './buzhash.js';
export {default as Commit} from './commit.js';

View File

@@ -230,12 +230,14 @@ suite('Path', () => {
test('parse errors', () => {
const t = (s: string, expectErr: string) => {
let actualErr = '';
try {
Path.parse(s);
assert.isOk(false, 'Expected error: ' + expectErr);
} catch (e) {
assert.strictEqual(expectErr, e.message);
assert.instanceOf(e, SyntaxError);
actualErr = e.message;
}
assert.strictEqual(expectErr, actualErr);
};
t('', 'Empty path');

View File

@@ -36,10 +36,15 @@ export interface Part {
/**
* A Path is an address to a Noms value - and unlike hashes (i.e. #abcd...) they can address inlined
* values. See https://github.com/attic-labs/noms/blob/master/doc/spelling.md.
*
* E.g. in a spec like `http://demo.noms.io::foo.bar` this is the `.bar` component.
*/
export default class Path {
_parts: Array<Part>;
/**
* Returns `str` parsed as Path. Throws a `SyntaxError` if `str` isn't a valid path.
*/
static parse(str: string): Path {
if (str === '') {
throw new SyntaxError('Empty path');

View File

@@ -18,7 +18,10 @@ import * as Bytes from './bytes.js';
type StructData = {[key: string]: Value};
/** Matches the first valid field name in a string. */
export const fieldNameComponentRe = /^[a-zA-Z][a-zA-Z0-9_]*/;
/** Matches if an entire string is a valid field name. */
export const fieldNameRe = new RegExp(fieldNameComponentRe.source + '$');
/**