mirror of
https://github.com/dolthub/dolt.git
synced 2026-02-12 02:58:53 -06:00
Merge pull request #1161 from arv/js-dynamic-struct
JS: Dynamically create a class per struct
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@attic/noms",
|
||||
"version": "6.1.0",
|
||||
"version": "7.0.0",
|
||||
"main": "dist/commonjs/noms.js",
|
||||
"jsnext:main": "dist/es6/noms.js",
|
||||
"dependencies": {
|
||||
|
||||
13
js/src/commit.js
Normal file
13
js/src/commit.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import type Struct from './struct.js';
|
||||
import type {valueOrPrimitive} from './value.js';
|
||||
import type RefValue from './ref-value.js';
|
||||
import type {NomsSet} from './set.js';
|
||||
|
||||
export interface Commit extends Struct {
|
||||
value: valueOrPrimitive; // readonly
|
||||
setValue(value: valueOrPrimitive): Commit;
|
||||
parents: NomsSet<RefValue<Commit>>; // readonly
|
||||
setParents(value: NomsSet<RefValue<Commit>>): Commit;
|
||||
}
|
||||
@@ -45,13 +45,13 @@ suite('DataStore', () => {
|
||||
|
||||
// The new datastore has |a|.
|
||||
const aCommit1 = notNull(await ds2.head(datasetID));
|
||||
assert.strictEqual('a', aCommit1.get('value'));
|
||||
assert.strictEqual('a', aCommit1.value);
|
||||
ds = ds2;
|
||||
|
||||
// |a| <- |b|
|
||||
const bCommit = await newCommit('b', [aCommit.ref]);
|
||||
ds = await ds.commit(datasetID, bCommit);
|
||||
assert.strictEqual('b', notNull(await ds.head(datasetID)).get('value'));
|
||||
assert.strictEqual('b', notNull(await ds.head(datasetID)).value);
|
||||
|
||||
// |a| <- |b|
|
||||
// \----|c|
|
||||
@@ -65,12 +65,12 @@ suite('DataStore', () => {
|
||||
message = ex.message;
|
||||
}
|
||||
assert.strictEqual('Merge needed', message);
|
||||
assert.strictEqual('b', notNull(await ds.head(datasetID)).get('value'));
|
||||
assert.strictEqual('b', notNull(await ds.head(datasetID)).value);
|
||||
|
||||
// |a| <- |b| <- |d|
|
||||
const dCommit = await newCommit('d', [bCommit.ref]);
|
||||
ds = await ds.commit(datasetID, dCommit);
|
||||
assert.strictEqual('d', notNull(await ds.head(datasetID)).get('value'));
|
||||
assert.strictEqual('d', notNull(await ds.head(datasetID)).value);
|
||||
|
||||
// Attempt to recommit |b| with |a| as parent.
|
||||
// Should be disallowed.
|
||||
@@ -81,16 +81,16 @@ suite('DataStore', () => {
|
||||
message = ex.message;
|
||||
}
|
||||
// assert.strictEqual('Merge needed', message);
|
||||
assert.strictEqual('d', notNull(await ds.head(datasetID)).get('value'));
|
||||
assert.strictEqual('d', notNull(await ds.head(datasetID)).value);
|
||||
|
||||
// Add a commit to a different datasetId
|
||||
ds = await ds.commit('otherDs', aCommit);
|
||||
assert.strictEqual('a', notNull(await ds.head('otherDs')).get('value'));
|
||||
assert.strictEqual('a', notNull(await ds.head('otherDs')).value);
|
||||
|
||||
// Get a fresh datastore, and verify that both datasets are present
|
||||
const newDs = new DataStore(ms);
|
||||
assert.strictEqual('d', notNull(await newDs.head(datasetID)).get('value'));
|
||||
assert.strictEqual('a', notNull(await newDs.head('otherDs')).get('value'));
|
||||
assert.strictEqual('d', notNull(await newDs.head(datasetID)).value);
|
||||
assert.strictEqual('a', notNull(await newDs.head('otherDs')).value);
|
||||
});
|
||||
|
||||
test('concurrency', async () => {
|
||||
@@ -103,7 +103,7 @@ suite('DataStore', () => {
|
||||
ds = await ds.commit(datasetID, aCommit);
|
||||
const bCommit = await newCommit('b', [aCommit.ref]);
|
||||
ds = await ds.commit(datasetID, bCommit);
|
||||
assert.strictEqual('b', notNull(await ds.head(datasetID)).get('value'));
|
||||
assert.strictEqual('b', notNull(await ds.head(datasetID)).value);
|
||||
|
||||
// Important to create this here.
|
||||
let ds2 = new DataStore(ms);
|
||||
@@ -112,7 +112,7 @@ suite('DataStore', () => {
|
||||
// |a| <- |b| <- |c|
|
||||
const cCommit = await newCommit('c', [bCommit.ref]);
|
||||
ds = await ds.commit(datasetID, cCommit);
|
||||
assert.strictEqual('c', notNull(await ds.head(datasetID)).get('value'));
|
||||
assert.strictEqual('c', notNull(await ds.head(datasetID)).value);
|
||||
|
||||
// Change 2:
|
||||
// |a| <- |b| <- |e|
|
||||
@@ -126,7 +126,7 @@ suite('DataStore', () => {
|
||||
message = ex.message;
|
||||
}
|
||||
assert.strictEqual('Merge needed', message);
|
||||
assert.strictEqual('c', notNull(await ds.head(datasetID)).get('value'));
|
||||
assert.strictEqual('c', notNull(await ds.head(datasetID)).value);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Chunk from './chunk.js';
|
||||
import Ref from './ref.js';
|
||||
import RefValue from './ref-value.js';
|
||||
import Struct from './struct.js';
|
||||
import {newStruct} from './struct.js';
|
||||
import type {ChunkStore} from './chunk-store.js';
|
||||
import type {NomsMap} from './map.js';
|
||||
import type {NomsSet} from './set.js';
|
||||
@@ -25,6 +25,7 @@ import {Package, registerPackage} from './package.js';
|
||||
import {decodeNomsValue} from './decode.js';
|
||||
import {invariant} from './assert.js';
|
||||
import {encodeNomsValue} from './encode.js';
|
||||
import type {Commit} from './commit.js';
|
||||
|
||||
type DatasTypes = {
|
||||
commitTypeDef: Type,
|
||||
@@ -35,15 +36,14 @@ type DatasTypes = {
|
||||
commitMapType: Type,
|
||||
};
|
||||
|
||||
let emptyCommitMap: Promise<NomsMap<string, RefValue<Struct>>>;
|
||||
function getEmptyCommitMap(): Promise<NomsMap<string, RefValue<Struct>>> {
|
||||
let emptyCommitMap: Promise<NomsMap<string, RefValue<Commit>>>;
|
||||
function getEmptyCommitMap(): Promise<NomsMap<string, RefValue<Commit>>> {
|
||||
if (!emptyCommitMap) {
|
||||
emptyCommitMap = newMap([], getDatasTypes().commitMapType);
|
||||
}
|
||||
return emptyCommitMap;
|
||||
}
|
||||
|
||||
|
||||
let datasTypes: DatasTypes;
|
||||
export function getDatasTypes(): DatasTypes {
|
||||
if (!datasTypes) {
|
||||
@@ -82,7 +82,7 @@ interface Cache<T> { // eslint-disable-line no-undef
|
||||
|
||||
export default class DataStore {
|
||||
_cs: ChunkStore;
|
||||
_datasets: Promise<NomsMap<string, RefValue<Struct>>>;
|
||||
_datasets: Promise<NomsMap<string, RefValue<Commit>>>;
|
||||
_valueCache: Cache<Promise<?valueOrPrimitive>>;
|
||||
|
||||
constructor(cs: ChunkStore, cacheSize: number = 0) {
|
||||
@@ -91,7 +91,7 @@ export default class DataStore {
|
||||
this._valueCache = cacheSize > 0 ? new SizeCache(cacheSize) : new NoopCache();
|
||||
}
|
||||
|
||||
_datasetsFromRootRef(rootRef: Promise<Ref>): Promise<NomsMap<string, RefValue<Struct>>> {
|
||||
_datasetsFromRootRef(rootRef: Promise<Ref>): Promise<NomsMap<string, RefValue<Commit>>> {
|
||||
return rootRef.then(rootRef => {
|
||||
if (rootRef.isEmpty()) {
|
||||
return getEmptyCommitMap();
|
||||
@@ -101,18 +101,18 @@ export default class DataStore {
|
||||
});
|
||||
}
|
||||
|
||||
head(datasetID: string): Promise<?Struct> {
|
||||
head(datasetID: string): Promise<?Commit> {
|
||||
return this._datasets.then(
|
||||
datasets => datasets.get(datasetID).then(commitRef =>
|
||||
commitRef ? this.readValue(commitRef.targetRef) : null));
|
||||
}
|
||||
|
||||
datasets(): Promise<NomsMap<string, RefValue<Struct>>> {
|
||||
datasets(): Promise<NomsMap<string, RefValue<Commit>>> {
|
||||
return this._datasets;
|
||||
}
|
||||
|
||||
async _descendsFrom(commit: Struct, currentHeadRef: RefValue<Struct>): Promise<boolean> {
|
||||
let ancestors = commit.get('parents');
|
||||
async _descendsFrom(commit: Commit, currentHeadRef: RefValue<Commit>): Promise<boolean> {
|
||||
let ancestors = commit.parents;
|
||||
while (!(await ancestors.has(currentHeadRef))) {
|
||||
if (ancestors.isEmpty()) {
|
||||
return false;
|
||||
@@ -167,7 +167,7 @@ export default class DataStore {
|
||||
return ref;
|
||||
}
|
||||
|
||||
async commit(datasetId: string, commit: Struct): Promise<DataStore> {
|
||||
async commit(datasetId: string, commit: Commit): Promise<DataStore> {
|
||||
const currentRootRefP = this._cs.getRoot();
|
||||
const datasetsP = this._datasetsFromRootRef(currentRootRefP);
|
||||
let currentDatasets = await (datasetsP:Promise<NomsMap>);
|
||||
@@ -197,22 +197,21 @@ export default class DataStore {
|
||||
}
|
||||
}
|
||||
|
||||
async function getAncestors(commits: NomsSet<RefValue<Struct>>, store: DataStore):
|
||||
Promise<NomsSet<RefValue<Struct>>> {
|
||||
async function getAncestors(commits: NomsSet<RefValue<Commit>>, store: DataStore):
|
||||
Promise<NomsSet<RefValue<Commit>>> {
|
||||
let ancestors = await newSet([], getDatasTypes().commitSetType);
|
||||
await commits.map(async (commitRef) => {
|
||||
const commit = await store.readValue(commitRef.targetRef);
|
||||
await commit.get('parents').map(async (ref) => ancestors = await ancestors.insert(ref));
|
||||
await commit.parents.map(async (ref) => ancestors = await ancestors.insert(ref));
|
||||
});
|
||||
return ancestors;
|
||||
}
|
||||
|
||||
export function newCommit(value: valueOrPrimitive, parents: Array<Ref> = []):
|
||||
Promise<Struct> {
|
||||
export function newCommit(value: valueOrPrimitive, parents: Array<Ref> = []): Promise<Commit> {
|
||||
const types = getDatasTypes();
|
||||
const parentRefs = parents.map(r => new RefValue(r, types.refOfCommitType));
|
||||
return newSet(parentRefs, types.commitSetType).then(parents =>
|
||||
new Struct(types.commitType, types.commitTypeDef, {value,parents}));
|
||||
newStruct(types.commitType, types.commitTypeDef, {value, parents}));
|
||||
}
|
||||
|
||||
class CacheEntry<T> {
|
||||
|
||||
@@ -21,12 +21,12 @@ suite('Dataset', () => {
|
||||
|
||||
// The new dataset has |a|.
|
||||
const aCommit = notNull(await ds2.head());
|
||||
assert.strictEqual('a', aCommit.get('value'));
|
||||
assert.strictEqual('a', aCommit.value);
|
||||
ds = ds2;
|
||||
|
||||
// |a| <- |b|
|
||||
ds = await ds.commit('b', [aCommit.ref]);
|
||||
assert.strictEqual('b', notNull(await ds.head()).get('value'));
|
||||
assert.strictEqual('b', notNull(await ds.head()).value);
|
||||
|
||||
// |a| <- |b|
|
||||
// \----|c|
|
||||
@@ -40,21 +40,21 @@ suite('Dataset', () => {
|
||||
invariant(ex instanceof Error);
|
||||
assert.strictEqual('Merge needed', ex.message);
|
||||
const bCommit = notNull(await ds.head());
|
||||
assert.strictEqual('b', bCommit.get('value'));
|
||||
assert.strictEqual('b', bCommit.value);
|
||||
|
||||
// |a| <- |b| <- |d|
|
||||
ds = await ds.commit('d');
|
||||
assert.strictEqual('d', notNull(await ds.head()).get('value'));
|
||||
assert.strictEqual('d', notNull(await ds.head()).value);
|
||||
|
||||
|
||||
// Add a commit to a different datasetId
|
||||
ds = new Dataset(store, 'otherDs');
|
||||
ds = await ds.commit('a');
|
||||
assert.strictEqual('a', notNull(await ds.head('otherDs')).get('value'));
|
||||
assert.strictEqual('a', notNull(await ds.head('otherDs')).value);
|
||||
|
||||
// Get a fresh datastore, and verify that both datasets are present
|
||||
const newStore = new DataStore(ms);
|
||||
assert.strictEqual('d', notNull(await newStore.head('ds1')).get('value'));
|
||||
assert.strictEqual('a', notNull(await newStore.head('otherDs')).get('value'));
|
||||
assert.strictEqual('d', notNull(await newStore.head('ds1')).value);
|
||||
assert.strictEqual('a', notNull(await newStore.head('otherDs')).value);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import {newCommit} from './data-store.js';
|
||||
import type {valueOrPrimitive} from './value.js';
|
||||
import type DataStore from './data-store.js';
|
||||
import type Struct from './struct.js';
|
||||
import type {Commit} from './commit.js';
|
||||
import type Ref from './ref.js';
|
||||
|
||||
export default class Dataset {
|
||||
@@ -23,7 +23,7 @@ export default class Dataset {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
head(): Promise<?Struct> {
|
||||
head(): Promise<?Commit> {
|
||||
return this._store.head(this._id);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class Dataset {
|
||||
const head = await this.head();
|
||||
parents = head ? [head.ref] : [];
|
||||
}
|
||||
const commit = await newCommit(v, parents);
|
||||
const commit: Commit = await newCommit(v, parents);
|
||||
const store = await this._store.commit(this._id, commit);
|
||||
return new Dataset(store, this._id);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import DataStore from './data-store.js';
|
||||
import MemoryStore from './memory-store.js';
|
||||
import Ref from './ref.js';
|
||||
import RefValue from './ref-value.js';
|
||||
import Struct from './struct.js';
|
||||
import {default as Struct, StructMirror} from './struct.js';
|
||||
import test from './async-test.js';
|
||||
import type {float64, int32, int64, uint8, uint16, uint32, uint64} from './primitives.js';
|
||||
import type {TypeDesc} from './type.js';
|
||||
@@ -252,10 +252,11 @@ suite('Decode', () => {
|
||||
function assertStruct(s: ?Struct, desc: TypeDesc, data: {[key: string]: any}) {
|
||||
notNull(s);
|
||||
invariant(s instanceof Struct, 'expected instanceof struct');
|
||||
assert.deepEqual(desc, s.desc);
|
||||
const mirror = new StructMirror(s);
|
||||
assert.deepEqual(desc, mirror.desc);
|
||||
|
||||
for (const key in data) {
|
||||
assert.deepEqual(data[key], s.get(key));
|
||||
assert.deepEqual(data[key], mirror.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +499,7 @@ suite('Decode', () => {
|
||||
const rootMap = await ds.readValue(root);
|
||||
const counterRef = await rootMap.get('counter');
|
||||
const commit = await counterRef.targetValue(ds);
|
||||
assert.strictEqual(1, await commit.get('value'));
|
||||
assert.strictEqual(1, await commit.value);
|
||||
});
|
||||
|
||||
test('out of line blob', async () => {
|
||||
|
||||
@@ -4,7 +4,8 @@ import {NomsBlob, BlobLeafSequence} from './blob.js';
|
||||
import Chunk from './chunk.js';
|
||||
import Ref from './ref.js';
|
||||
import RefValue from './ref-value.js';
|
||||
import Struct from './struct.js';
|
||||
import {newStruct} from './struct.js';
|
||||
import type Struct from './struct.js';
|
||||
import type DataStore from './data-store.js';
|
||||
import type {NomsKind} from './noms-kind.js';
|
||||
import {decode as decodeBase64} from './base64.js';
|
||||
@@ -407,12 +408,11 @@ export class JsonArrayReader {
|
||||
|
||||
}
|
||||
|
||||
readStruct(typeDef: Type, type: Type, pkg: Package): Struct {
|
||||
// TODO FixupType?
|
||||
readStruct<T: Struct>(typeDef: Type, type: Type, pkg: Package): T {
|
||||
const desc = typeDef.desc;
|
||||
invariant(desc instanceof StructDesc);
|
||||
|
||||
const s: { [key: string]: any } = Object.create(null);
|
||||
const data: {[key: string]: any} = Object.create(null);
|
||||
|
||||
for (let i = 0; i < desc.fields.length; i++) {
|
||||
const field = desc.fields[i];
|
||||
@@ -420,11 +420,11 @@ export class JsonArrayReader {
|
||||
const b = this.readBool();
|
||||
if (b) {
|
||||
const v = this.readValueWithoutTag(field.t, pkg);
|
||||
s[field.name] = v;
|
||||
data[field.name] = v;
|
||||
}
|
||||
} else {
|
||||
const v = this.readValueWithoutTag(field.t, pkg);
|
||||
s[field.name] = v;
|
||||
data[field.name] = v;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,11 +433,11 @@ export class JsonArrayReader {
|
||||
unionIndex = this.readUint();
|
||||
const unionField = desc.union[unionIndex];
|
||||
const v = this.readValueWithoutTag(unionField.t, pkg);
|
||||
s[unionField.name] = v;
|
||||
data[unionField.name] = v;
|
||||
}
|
||||
|
||||
type = fixupType(type, pkg);
|
||||
return new Struct(type, typeDef, s);
|
||||
return newStruct(type, typeDef, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Chunk from './chunk.js';
|
||||
import type Ref from './ref.js';
|
||||
import RefValue from './ref-value.js';
|
||||
import Struct from './struct.js';
|
||||
import {default as Struct, StructMirror} from './struct.js';
|
||||
import type DataStore from './data-store.js';
|
||||
import type {NomsKind} from './noms-kind.js';
|
||||
import {encode as encodeBase64} from './base64.js';
|
||||
@@ -330,28 +330,25 @@ export class JsonArrayWriter {
|
||||
}
|
||||
|
||||
writeStruct(s: Struct, type: Type, typeDef: Type, pkg: Package) {
|
||||
const desc = typeDef.desc;
|
||||
invariant(desc instanceof StructDesc);
|
||||
for (let i = 0; i < desc.fields.length; i++) {
|
||||
const field = desc.fields[i];
|
||||
const fieldValue = s.get(field.name);
|
||||
const mirror = new StructMirror(s);
|
||||
mirror.forEachField(field => {
|
||||
if (field.optional) {
|
||||
if (fieldValue !== undefined) {
|
||||
if (field.present) {
|
||||
this.writeBoolean(true);
|
||||
this.writeValue(fieldValue, field.t, pkg);
|
||||
this.writeValue(field.value, field.type, pkg);
|
||||
} else {
|
||||
this.writeBoolean(false);
|
||||
}
|
||||
} else {
|
||||
invariant(fieldValue !== undefined);
|
||||
this.writeValue(s.get(field.name), field.t, pkg);
|
||||
invariant(field.present);
|
||||
this.writeValue(field.value, field.type, pkg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (s.hasUnion) {
|
||||
const unionField = notNull(desc.union[s.unionIndex]);
|
||||
this.writeInt(s.unionIndex);
|
||||
this.writeValue(s.get(unionField.name), unionField.t, pkg);
|
||||
if (mirror.hasUnion) {
|
||||
const {unionField} = mirror;
|
||||
this.writeInt(mirror.unionIndex);
|
||||
this.writeValue(unionField.value, unionField.type, pkg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
|
||||
import MemoryStore from './memory-store.js';
|
||||
import RefValue from './ref-value.js';
|
||||
import Struct from './struct.js';
|
||||
import {newStruct, StructMirror, createStructClass} from './struct.js';
|
||||
import {assert} from 'chai';
|
||||
import {Field, makeCompoundType, makePrimitiveType, makeStructType, makeType} from './type.js';
|
||||
import {
|
||||
Field,
|
||||
makeCompoundType,
|
||||
makePrimitiveType,
|
||||
makeStructType,
|
||||
makeType,
|
||||
float64Type,
|
||||
stringType,
|
||||
} from './type.js';
|
||||
import {Kind} from './noms-kind.js';
|
||||
import {Package, registerPackage} from './package.js';
|
||||
import {suite, test} from 'mocha';
|
||||
import DataStore from './data-store.js';
|
||||
import Ref from './ref.js';
|
||||
|
||||
suite('Struct', () => {
|
||||
test('equals', () => {
|
||||
@@ -23,8 +32,8 @@ suite('Struct', () => {
|
||||
const type = makeType(pkgRef, 0);
|
||||
|
||||
const data1 = {x: true};
|
||||
const s1 = new Struct(type, typeDef, data1);
|
||||
const s2 = new Struct(type, typeDef, data1);
|
||||
const s1 = newStruct(type, typeDef, data1);
|
||||
const s2 = newStruct(type, typeDef, data1);
|
||||
|
||||
assert.isTrue(s1.equals(s2));
|
||||
});
|
||||
@@ -46,7 +55,7 @@ suite('Struct', () => {
|
||||
|
||||
const b = true;
|
||||
const r = new RefValue(ds.writeValue(b), refOfBoolType);
|
||||
const s1 = new Struct(type, typeDef, {r: r});
|
||||
const s1 = newStruct(type, typeDef, {r: r});
|
||||
assert.strictEqual(2, s1.chunks.length);
|
||||
assert.isTrue(pkgRef.equals(s1.chunks[0].targetRef));
|
||||
assert.isTrue(r.equals(s1.chunks[1]));
|
||||
@@ -67,14 +76,14 @@ suite('Struct', () => {
|
||||
const pkgRef = pkg.ref;
|
||||
const type = makeType(pkgRef, 0);
|
||||
|
||||
const s1 = new Struct(type, typeDef, {});
|
||||
const s1 = newStruct(type, typeDef, {});
|
||||
|
||||
assert.strictEqual(1, s1.chunks.length);
|
||||
assert.isTrue(pkgRef.equals(s1.chunks[0].targetRef));
|
||||
|
||||
const b = true;
|
||||
const r = new RefValue(ds.writeValue(b), refOfBoolType);
|
||||
const s2 = new Struct(type, typeDef, {r: r});
|
||||
const s2 = newStruct(type, typeDef, {r: r});
|
||||
assert.strictEqual(2, s2.chunks.length);
|
||||
assert.isTrue(pkgRef.equals(s2.chunks[0].targetRef));
|
||||
assert.isTrue(r.equals(s2.chunks[1]));
|
||||
@@ -96,13 +105,13 @@ suite('Struct', () => {
|
||||
const pkgRef = pkg.ref;
|
||||
const type = makeType(pkgRef, 0);
|
||||
|
||||
const s1 = new Struct(type, typeDef, {s: 'hi'});
|
||||
const s1 = newStruct(type, typeDef, {s: 'hi'});
|
||||
assert.strictEqual(1, s1.chunks.length);
|
||||
assert.isTrue(pkgRef.equals(s1.chunks[0].targetRef));
|
||||
|
||||
const b = true;
|
||||
const r = new RefValue(ds.writeValue(b), refOfBoolType);
|
||||
const s2 = new Struct(type, typeDef, {r: r});
|
||||
const s2 = newStruct(type, typeDef, {r: r});
|
||||
assert.strictEqual(2, s2.chunks.length);
|
||||
assert.isTrue(pkgRef.equals(s2.chunks[0].targetRef));
|
||||
assert.isTrue(r.equals(s2.chunks[1]));
|
||||
@@ -119,23 +128,24 @@ suite('Struct', () => {
|
||||
const pkgRef = pkg.ref;
|
||||
const type = makeType(pkgRef, 0);
|
||||
|
||||
const s1 = new Struct(type, typeDef, {b: true});
|
||||
assert.strictEqual(true, s1.get('b'));
|
||||
assert.isFalse(s1.has('o'));
|
||||
assert.isFalse(s1.has('x'));
|
||||
const s1 = newStruct(type, typeDef, {b: true});
|
||||
assert.strictEqual(true, s1.b);
|
||||
assert.strictEqual(s1.o, undefined);
|
||||
|
||||
const s2 = new Struct(type, typeDef, {b: false, o: 'hi'});
|
||||
assert.strictEqual(false, s2.get('b'));
|
||||
assert.isTrue(s2.has('o'));
|
||||
assert.strictEqual('hi', s2.get('o'));
|
||||
const s2 = newStruct(type, typeDef, {b: false, o: 'hi'});
|
||||
assert.strictEqual(false, s2.b);
|
||||
assert.strictEqual('hi', s2.o);
|
||||
|
||||
assert.throws(() => {
|
||||
new Struct(type, typeDef, {o: 'hi'}); // missing required field
|
||||
newStruct(type, typeDef, {o: 'hi'}); // missing required field
|
||||
});
|
||||
|
||||
assert.throws(() => {
|
||||
new Struct(type, typeDef, {x: 'hi'}); // unknown field
|
||||
newStruct(type, typeDef, {x: 'hi'}); // unknown field
|
||||
});
|
||||
|
||||
const s3 = newStruct(type, typeDef, {b: true, o: undefined});
|
||||
assert.isTrue(s1.equals(s3));
|
||||
});
|
||||
|
||||
test('new union', () => {
|
||||
@@ -149,9 +159,9 @@ suite('Struct', () => {
|
||||
const pkgRef = pkg.ref;
|
||||
const type = makeType(pkgRef, 0);
|
||||
|
||||
const s1 = new Struct(type, typeDef, {b: true});
|
||||
assert.strictEqual(true, s1.get('b'));
|
||||
assert.isFalse(s1.has('o'));
|
||||
const s1 = newStruct(type, typeDef, {b: true});
|
||||
assert.strictEqual(true, s1.b);
|
||||
assert.strictEqual(s1.o, undefined);
|
||||
});
|
||||
|
||||
test('struct set', () => {
|
||||
@@ -165,16 +175,24 @@ suite('Struct', () => {
|
||||
const pkgRef = pkg.ref;
|
||||
const type = makeType(pkgRef, 0);
|
||||
|
||||
const s1 = new Struct(type, typeDef, {b: true});
|
||||
const s2 = s1.set('b', false);
|
||||
const s1 = newStruct(type, typeDef, {b: true});
|
||||
const s2 = s1.setB(false);
|
||||
|
||||
// TODO: assert throws on set wrong type
|
||||
assert.throws(() => {
|
||||
s1.set('x', 1);
|
||||
s1.setX(1);
|
||||
});
|
||||
|
||||
const s3 = s2.set('b', true);
|
||||
const s3 = s2.setB(true);
|
||||
assert.isTrue(s1.equals(s3));
|
||||
|
||||
const m = new StructMirror(s1);
|
||||
const s4 = m.set('b', false);
|
||||
assert.isTrue(s2.equals(s4));
|
||||
|
||||
const s5 = s3.setO(undefined);
|
||||
const s6 = new StructMirror(s3).set('o', undefined);
|
||||
assert.isTrue(s5.equals(s6));
|
||||
});
|
||||
|
||||
test('struct set union', () => {
|
||||
@@ -188,17 +206,20 @@ suite('Struct', () => {
|
||||
const pkgRef = pkg.ref;
|
||||
const type = makeType(pkgRef, 0);
|
||||
|
||||
const s1 = new Struct(type, typeDef, {b: true});
|
||||
assert.strictEqual(0, s1.unionIndex);
|
||||
assert.strictEqual(true, s1.unionValue);
|
||||
assert.isFalse(s1.has('s'));
|
||||
const s1 = newStruct(type, typeDef, {b: true});
|
||||
const m1 = new StructMirror(s1);
|
||||
assert.strictEqual(0, m1.unionIndex);
|
||||
assert.strictEqual(true, m1.unionValue);
|
||||
assert.strictEqual(s1.s, undefined);
|
||||
|
||||
const s2 = s1.set('s', 'hi');
|
||||
assert.strictEqual(1, s2.unionIndex);
|
||||
assert.strictEqual('hi', s2.unionValue);
|
||||
assert.isFalse(s2.has('b'));
|
||||
const s2 = s1.setS('hi');
|
||||
const m2 = new StructMirror(s2);
|
||||
assert.strictEqual(1, m2.unionIndex);
|
||||
assert.strictEqual('hi', m2.unionValue);
|
||||
assert.strictEqual(s2.b, undefined);
|
||||
assert.isFalse(m2.has('b'));
|
||||
|
||||
const s3 = s2.set('b', true);
|
||||
const s3 = s2.setB(true);
|
||||
assert.isTrue(s1.equals(s3));
|
||||
});
|
||||
|
||||
@@ -213,11 +234,59 @@ suite('Struct', () => {
|
||||
const type = makeType(pkgRef, 0);
|
||||
|
||||
assert.throws(() => {
|
||||
new Struct(type, type, {b: true});
|
||||
newStruct(type, type, {b: true});
|
||||
});
|
||||
|
||||
assert.throws(() => {
|
||||
new Struct(typeDef, typeDef, {b: true});
|
||||
newStruct(typeDef, typeDef, {b: true});
|
||||
});
|
||||
});
|
||||
|
||||
test('named union', () => {
|
||||
let typeDef, typeDefA, typeDefD;
|
||||
const pkg = new Package([
|
||||
typeDef = makeStructType('StructWithUnions', [
|
||||
new Field('a', makeType(new Ref(), 1), false),
|
||||
new Field('d', makeType(new Ref(), 2), false),
|
||||
], []),
|
||||
typeDefA = makeStructType('', [], [
|
||||
new Field('b', float64Type, false),
|
||||
new Field('c', stringType, false),
|
||||
]),
|
||||
typeDefD = makeStructType('', [], [
|
||||
new Field('e', float64Type, false),
|
||||
new Field('f', stringType, false),
|
||||
]),
|
||||
], []);
|
||||
registerPackage(pkg);
|
||||
const pkgRef = pkg.ref;
|
||||
const type = makeType(pkgRef, 0);
|
||||
const typeA = makeType(pkgRef, 1);
|
||||
const typeD = makeType(pkgRef, 2);
|
||||
|
||||
const StructWithUnions = createStructClass(type, typeDef);
|
||||
const A = createStructClass(typeA, typeDefA);
|
||||
const D = createStructClass(typeD, typeDefD);
|
||||
|
||||
const s = new StructWithUnions({
|
||||
a: new A({b: 1}),
|
||||
d: new D({e: 2}),
|
||||
});
|
||||
|
||||
assert.equal(s.a.b, 1);
|
||||
assert.equal(s.d.e, 2);
|
||||
|
||||
const s2 = s.setA(s.a.setC('hi'));
|
||||
assert.equal(s2.a.c, 'hi');
|
||||
assert.equal(s2.a.b, undefined);
|
||||
|
||||
const s3 = s2.setD(s.d.setF('bye'));
|
||||
assert.equal(s3.d.f, 'bye');
|
||||
assert.equal(s3.d.e, undefined);
|
||||
|
||||
assert.isTrue(s3.equals(new StructWithUnions({
|
||||
a: new A({c: 'hi'}),
|
||||
d: new D({f: 'bye'}),
|
||||
})));
|
||||
});
|
||||
});
|
||||
|
||||
335
js/src/struct.js
335
js/src/struct.js
@@ -4,19 +4,37 @@ import type RefValue from './ref-value.js';
|
||||
import type {valueOrPrimitive} from './value.js';
|
||||
import {StructDesc} from './type.js';
|
||||
import type {Field, Type} from './type.js';
|
||||
import {invariant, notNull} from './assert.js';
|
||||
import {invariant} from './assert.js';
|
||||
import {isPrimitive} from './primitives.js';
|
||||
import {Kind} from'./noms-kind.js';
|
||||
import {ValueBase} from './value.js';
|
||||
|
||||
type StructData = {[key: string]: valueOrPrimitive};
|
||||
type StructData = {[key: string]: ?valueOrPrimitive};
|
||||
|
||||
/**
|
||||
* Base class for all Noms structs. The decoder creates sub classes of this for Noms struct.
|
||||
* These have the form of:
|
||||
*
|
||||
* ```noms
|
||||
* struct MyStruct {
|
||||
* x: Int8
|
||||
* s: string
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* interface MyStruct extends Struct {
|
||||
* get x(): int8;
|
||||
* setX(value: int8): MyStruct;
|
||||
* get s(): string;
|
||||
* setS(value: string): MyStruct;
|
||||
* }
|
||||
*
|
||||
* To reflect over structs you can create a new StructMirror.
|
||||
*/
|
||||
export default class Struct extends ValueBase {
|
||||
desc: StructDesc;
|
||||
_unionIndex: number;
|
||||
|
||||
_data: StructData;
|
||||
typeDef: Type;
|
||||
_typeDef: Type;
|
||||
|
||||
constructor(type: Type, typeDef: Type, data: StructData) {
|
||||
super(type);
|
||||
@@ -24,131 +42,258 @@ export default class Struct extends ValueBase {
|
||||
invariant(type.kind === Kind.Unresolved);
|
||||
invariant(typeDef.kind === Kind.Struct);
|
||||
|
||||
this.typeDef = typeDef;
|
||||
|
||||
const desc = typeDef.desc;
|
||||
invariant(desc instanceof StructDesc);
|
||||
this.desc = desc;
|
||||
// TODO: Even in dev mode there are paths where the passed in data has already been validated.
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
validate(typeDef, data);
|
||||
}
|
||||
|
||||
this._typeDef = typeDef;
|
||||
this._data = data;
|
||||
this._unionIndex = validate(this);
|
||||
}
|
||||
|
||||
get chunks(): Array<RefValue> {
|
||||
const mirror = new StructMirror(this);
|
||||
const chunks = [];
|
||||
chunks.push(...this.type.chunks);
|
||||
forEach(this, this._unionField, v => {
|
||||
if (!isPrimitive(v)) {
|
||||
chunks.push(...v.chunks);
|
||||
|
||||
const add = field => {
|
||||
if (!field.present) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
const {value} = field;
|
||||
if (!isPrimitive(value)) {
|
||||
invariant(value instanceof ValueBase);
|
||||
chunks.push(...value.chunks);
|
||||
}
|
||||
};
|
||||
|
||||
mirror.forEachField(add);
|
||||
if (mirror.hasUnion) {
|
||||
add(mirror.unionField);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
|
||||
function validate(typeDef: Type, data: StructData): void {
|
||||
// TODO: Validate field values match field types.
|
||||
const {desc} = typeDef;
|
||||
invariant(desc instanceof StructDesc);
|
||||
const {fields} = desc;
|
||||
let dataCount = Object.keys(data).length;
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i];
|
||||
invariant(data[field.name] !== undefined || field.optional);
|
||||
if (field.name in data) {
|
||||
dataCount--;
|
||||
}
|
||||
}
|
||||
|
||||
const {union} = desc;
|
||||
if (union.length > 0) {
|
||||
invariant(dataCount === 1);
|
||||
for (let i = 0; i < union.length; i++) {
|
||||
const field = union[i];
|
||||
if (data[field.name] !== undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
invariant(false);
|
||||
} else {
|
||||
invariant(dataCount === 0);
|
||||
}
|
||||
}
|
||||
|
||||
export function findUnionIndex(data: StructData, union: Array<Field>): number {
|
||||
for (let i = 0; i < union.length; i++) {
|
||||
const field = union[i];
|
||||
if (data[field.name] !== undefined) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
class StructFieldMirror {
|
||||
value: ?valueOrPrimitive;
|
||||
_f: Field;
|
||||
|
||||
constructor(data: StructData, f: Field) {
|
||||
this.value = data[f.name];
|
||||
this._f = f;
|
||||
}
|
||||
get name() {
|
||||
return this._f.name;
|
||||
}
|
||||
get type() {
|
||||
return this._f.t;
|
||||
}
|
||||
get optional() {
|
||||
return this._f.optional;
|
||||
}
|
||||
get present() {
|
||||
return this.value !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
type FieldCallback = (f: StructFieldMirror) => void;
|
||||
|
||||
export class StructMirror<T: Struct> {
|
||||
_data: StructData;
|
||||
_type :Type;
|
||||
typeDef: Type;
|
||||
|
||||
constructor(s: Struct) {
|
||||
this._data = s._data;
|
||||
this._type = s.type;
|
||||
this.typeDef = s._typeDef;
|
||||
}
|
||||
|
||||
get desc(): StructDesc {
|
||||
invariant(this.typeDef.desc instanceof StructDesc);
|
||||
return this.typeDef.desc;
|
||||
}
|
||||
|
||||
forEachField(cb: FieldCallback) {
|
||||
this.desc.fields.forEach(field => cb(new StructFieldMirror(this._data, field)));
|
||||
}
|
||||
|
||||
get hasUnion(): boolean {
|
||||
return this.desc.union.length > 0;
|
||||
}
|
||||
|
||||
get unionIndex(): number {
|
||||
return this._unionIndex;
|
||||
return findUnionIndex(this._data, this.desc.union);
|
||||
}
|
||||
|
||||
get unionField(): StructFieldMirror {
|
||||
invariant(this.hasUnion);
|
||||
return new StructFieldMirror(this._data, this.desc.union[this.unionIndex]);
|
||||
}
|
||||
|
||||
get unionValue(): ?valueOrPrimitive {
|
||||
return this._data[this._unionField.name];
|
||||
return this._data[this.unionField.name];
|
||||
}
|
||||
|
||||
get _unionField(): Field {
|
||||
return this.desc.union[this._unionIndex];
|
||||
get(name: string): ?valueOrPrimitive {
|
||||
return this._data[name];
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this._data[key] !== undefined;
|
||||
has(name: string): boolean {
|
||||
return this.get(name) !== undefined;
|
||||
}
|
||||
|
||||
get(key: string): any {
|
||||
return this._data[key];
|
||||
set(name: string, value: ?valueOrPrimitive): T {
|
||||
const data = addProperty(this, name, value);
|
||||
return newStruct(this._type, this.typeDef, data);
|
||||
}
|
||||
}
|
||||
|
||||
const cache: {[key: string]: Class<any>} = Object.create(null);
|
||||
|
||||
function setterName(name) {
|
||||
return `set${name[0].toUpperCase()}${name.slice(1)}`;
|
||||
}
|
||||
|
||||
export function createStructClass<T: Struct>(type: Type, typeDef: Type): Class<T> {
|
||||
const k = type.ref.toString();
|
||||
if (cache[k]) {
|
||||
return cache[k];
|
||||
}
|
||||
|
||||
set(key: string, value: any): Struct {
|
||||
let [f, unionIndex] = findField(this.desc, key); // eslint-disable-line prefer-const
|
||||
f = notNull(f);
|
||||
const c: any = class extends Struct {
|
||||
constructor(data: StructData) {
|
||||
super(type, typeDef, data);
|
||||
}
|
||||
};
|
||||
|
||||
const data = Object.create(null);
|
||||
this.desc.fields.forEach(f => {
|
||||
const v = this._data[f.name];
|
||||
if (v !== undefined) {
|
||||
data[f.name] = v;
|
||||
const {desc} = typeDef;
|
||||
invariant(desc instanceof StructDesc);
|
||||
|
||||
for (const fields of [desc.fields, desc.union]) {
|
||||
for (const field of fields) {
|
||||
const {name} = field;
|
||||
Object.defineProperty(c.prototype, name, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: function() {
|
||||
return this._data[name];
|
||||
},
|
||||
});
|
||||
Object.defineProperty(c.prototype, setterName(name), {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: getSetter(name, field.optional, fields === desc.union),
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cache[k] = c;
|
||||
}
|
||||
|
||||
function getSetter(name: string, optional: boolean, union: boolean) {
|
||||
if (!optional && !union) {
|
||||
return function(value) {
|
||||
const newData = Object.assign(Object.create(null), this._data);
|
||||
newData[name] = value;
|
||||
return new this.constructor(newData);
|
||||
};
|
||||
}
|
||||
if (optional && !union) {
|
||||
return function(value) {
|
||||
const newData = Object.assign(Object.create(null), this._data);
|
||||
if (value === undefined) {
|
||||
delete newData[name];
|
||||
} else {
|
||||
newData[name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
data[key] = value;
|
||||
if (unionIndex === -1 && this.hasUnion) {
|
||||
const unionName = this.desc.union[this._unionIndex].name;
|
||||
data[unionName] = this._data[unionName];
|
||||
}
|
||||
|
||||
return new Struct(this.type, this.typeDef, data);
|
||||
return new this.constructor(newData);
|
||||
};
|
||||
}
|
||||
return function(value) {
|
||||
const data = addProperty(new StructMirror(this), name, value);
|
||||
return new this.constructor(data);
|
||||
};
|
||||
}
|
||||
|
||||
function findField(desc: StructDesc, name: string): [?Field, number] {
|
||||
for (let i = 0; i < desc.fields.length; i++) {
|
||||
const f = desc.fields[i];
|
||||
function addProperty(mirror: StructMirror, name: string, value: ?valueOrPrimitive): StructData {
|
||||
const data = Object.create(null);
|
||||
let found = false;
|
||||
mirror.forEachField(f => {
|
||||
if (f.name === name) {
|
||||
return [f, -1];
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < desc.union.length; i++) {
|
||||
const f = desc.union[i];
|
||||
if (f.name === name) {
|
||||
return [f, i];
|
||||
}
|
||||
}
|
||||
|
||||
return [null, -1];
|
||||
}
|
||||
|
||||
function validate(s: Struct): number {
|
||||
// TODO: Validate field values match field types.
|
||||
const data = s._data;
|
||||
let dataCount = Object.keys(data).length;
|
||||
for (let i = 0; i < s.desc.fields.length; i++) {
|
||||
const field = s.desc.fields[i];
|
||||
if (data[field.name] !== undefined) {
|
||||
dataCount--;
|
||||
} else {
|
||||
invariant(field.optional);
|
||||
}
|
||||
}
|
||||
|
||||
if (s.desc.union.length > 0) {
|
||||
invariant(dataCount === 1);
|
||||
for (let i = 0; i < s.desc.union.length; i++) {
|
||||
const field = s.desc.union[i];
|
||||
if (data[field.name] !== undefined) {
|
||||
return i;
|
||||
if (value !== undefined) {
|
||||
data[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
invariant(false);
|
||||
} else {
|
||||
invariant(dataCount === 0);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function forEach(struct: Struct,
|
||||
unionField: ?Field,
|
||||
callbackfn: (value: any, index: string, field?: Field) => void): void {
|
||||
struct.desc.fields.forEach(field => {
|
||||
const fieldValue = struct._data[field.name];
|
||||
if (fieldValue !== undefined) {
|
||||
callbackfn(struct._data[field.name], field.name, field);
|
||||
found = true;
|
||||
} else if (f.present) {
|
||||
data[f.name] = f.value;
|
||||
}
|
||||
});
|
||||
|
||||
if (unionField) {
|
||||
callbackfn(struct._data[unionField.name], unionField.name, unionField);
|
||||
if (mirror.hasUnion) {
|
||||
if (found) {
|
||||
const {unionField} = mirror;
|
||||
data[unionField.name] = unionField.value;
|
||||
} else {
|
||||
const {union} = mirror.desc;
|
||||
for (let i = 0; i < union.length; i++) {
|
||||
if (union[i].name === name) {
|
||||
data[name] = value;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invariant(found);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function newStruct<T: Struct>(type: Type, typeDef: Type, data: StructData): T {
|
||||
const c = createStructClass(type, typeDef);
|
||||
return new c(data);
|
||||
}
|
||||
|
||||
@@ -192,6 +192,7 @@ export class Field {
|
||||
|
||||
constructor(name: string, t: Type, optional: boolean) {
|
||||
this.name = name;
|
||||
// TODO: Rename this to type.
|
||||
this.t = t;
|
||||
this.optional = optional;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ export class ValueBase {
|
||||
less(other: Value): boolean {
|
||||
return this.ref.less(other.ref);
|
||||
}
|
||||
|
||||
get chunks(): Array<RefValue> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export type valueOrPrimitive = primitive | Value;
|
||||
|
||||
Reference in New Issue
Block a user