Merge pull request #1161 from arv/js-dynamic-struct

JS: Dynamically create a class per struct
This commit is contained in:
Erik Arvidsson
2016-04-01 09:44:54 -07:00
13 changed files with 427 additions and 198 deletions

View File

@@ -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
View 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;
}

View File

@@ -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);
});

View File

@@ -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> {

View File

@@ -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);
});
});

View File

@@ -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);
}

View File

@@ -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 () => {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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'}),
})));
});
});

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;