mirror of
https://github.com/dolthub/dolt.git
synced 2026-02-05 02:59:44 -06:00
Port AbsolutePath implementation to JS (#2325)
This commit is contained in:
100
js/src/absolute-path-test.js
Normal file
100
js/src/absolute-path-test.js
Normal 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
107
js/src/absolute-path.js
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 + '$');
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user