Copy v1/util to v3/util (#373)

No changes made to the code - it's a straight copy.

Test plan:
Unit tests are included.
This commit is contained in:
Dandelion Mané 2018-06-11 10:42:25 -07:00 committed by GitHub
parent 831f5f571c
commit 5fde1c10a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 213 additions and 0 deletions

45
src/v3/util/compat.js Normal file
View File

@ -0,0 +1,45 @@
// @flow
export opaque type Compatible<T> = [CompatInfo, T];
type CompatInfo = {|
+type: string,
+version: string,
|};
export function toCompat<T>(compatInfo: CompatInfo, obj: T): Compatible<T> {
return [compatInfo, obj];
}
/**
* Load an object from compatibilized state created by `toCompat`.
* The object has an expected type and version, and may optionally have
* handler functions for transforming previous versions into a canonical state.
* If a handler is present for the current version, it will be applied.
* Throws an error if the compatibilized object is the wrong type, or if its version
* is not current and there was no handler for its version.
*/
export function fromCompat<T>(
expectedCompatInfo: CompatInfo,
obj: Compatible<any>,
handlers: ?{[version: string]: (any) => T}
): T {
if (!Array.isArray(obj) || obj.length !== 2) {
throw new Error(
"Tried to load object that didn't have compatibility defined"
);
}
const {type, version} = obj[0];
let result = obj[1];
const {type: expectedType, version: expectedVersion} = expectedCompatInfo;
if (type !== expectedType) {
throw new Error(`Expected type to be ${expectedType} but got ${type}`);
}
if (handlers != null && handlers[version] != null) {
result = handlers[version](result);
} else if (version !== expectedVersion) {
throw new Error(`${type}: tried to load unsupported version ${version}`);
}
return result;
}

168
src/v3/util/compat.test.js Normal file
View File

@ -0,0 +1,168 @@
// @flow
import {toCompat, fromCompat} from "./compat";
import type {Compatible} from "./compat";
describe("compat utilities", () => {
type FooV1 = {foo: number};
type FooV2 = {bar: {foo: number}};
const type = "Foo Plugin's Experiment";
const v1 = "v1";
const v2 = "v2";
function fooV1ToV2(x: FooV1): FooV2 {
return {bar: x};
}
const example = () => {
const dataV1: FooV1 = {foo: 1};
const dataV2: FooV2 = {bar: {foo: 1}};
const compatV1 = toCompat({type, version: v1}, dataV1);
const compatV2 = toCompat({type, version: v2}, dataV2);
return {dataV1, dataV2, compatV1, compatV2};
};
it("toCompat doesn't fails on primitives", () => {
expect(
fromCompat({type, version: v1}, toCompat({type, version: v1}, 3))
).toBe(3);
});
it("toCompat -> fromCompat is identity", () => {
const {dataV1, compatV1} = example();
expect(fromCompat({type, version: v1}, compatV1)).toEqual(dataV1);
});
it("fromCompat fails if compatibility undefined", () => {
const dataV1: any = example().dataV1;
expect(() => fromCompat({type, version: v1}, dataV1)).toThrowError(
"didn't have compatibility defined"
);
});
it("fromCompat fails if type is wrong", () => {
const {compatV1} = example();
expect(() =>
fromCompat({type: "who is Foo?", version: v1}, compatV1)
).toThrowError("Expected type");
});
it("fromCompat fails if version is wrong", () => {
const {compatV1} = example();
expect(() => fromCompat({type, version: v2}, compatV1)).toThrowError(
"unsupported version"
);
});
it("handlers can load older versions", () => {
const {compatV1, dataV2} = example();
const handlers = {[v1]: fooV1ToV2};
expect(fromCompat({type: type, version: v2}, compatV1, handlers)).toEqual(
dataV2
);
});
it("handlers activate even on current version", () => {
const {compatV1} = example();
const handlers = {
[v1]: () => ({
hello: "world",
}),
};
expect(fromCompat({type, version: v1}, compatV1, handlers)).toEqual({
hello: "world",
});
});
describe("composable versioning", () => {
class InnerV1 {
x: number;
constructor(x: number) {
this.x = x;
}
toJSON(): Compatible<FooV1> {
return toCompat({type: "inner", version: v1}, {foo: this.x});
}
static fromJSON(json): InnerV1 {
const from: FooV1 = fromCompat({type: "inner", version: v1}, json);
return new InnerV1(from.foo);
}
}
class InnerV2 {
x: number;
constructor(x: number) {
this.x = x;
}
toJSON(): Compatible<FooV2> {
return toCompat({type: "inner", version: v2}, {bar: {foo: this.x}});
}
static fromJSON(json): InnerV2 {
const from: FooV2 = fromCompat({type: "inner", version: v2}, json, {
[v1]: fooV1ToV2,
});
return new InnerV2(from.bar.foo);
}
}
class OuterV1 {
platypus: InnerV1 | InnerV2;
constructor(i: InnerV1 | InnerV2) {
this.platypus = i;
}
toJSON() {
return toCompat(
{type: "outer", version: v1},
{platypus: this.platypus.toJSON()}
);
}
fromJSON(json: any): OuterV1 {
return fromCompat({type: "outer", version: v1}, json, {
[v1]: function(x) {
return new OuterV1(InnerV2.fromJSON(x.platypus));
},
});
}
}
class OuterV2 {
// Naming this property "platypus" in the previous version was silly
inner: InnerV1 | InnerV2;
constructor(i: InnerV1 | InnerV2) {
this.inner = i;
}
toJSON() {
return toCompat(
{type: "outer", version: v2},
{inner: this.inner.toJSON()}
);
}
static fromJSON(json: any): OuterV2 {
return fromCompat({type: "outer", version: v2}, json, {
[v1]: function(x) {
return new OuterV2(InnerV2.fromJSON(x.platypus));
},
[v2]: function(x) {
return new OuterV2(InnerV2.fromJSON(x.inner));
},
});
}
}
const canonical = () => new OuterV2(new InnerV2(1));
it("loads OuterV1<InnerV1>", () => {
const json = new OuterV1(new InnerV1(1)).toJSON();
expect(OuterV2.fromJSON(json)).toEqual(canonical());
});
it("loads OuterV1<InnerV2>", () => {
const json = new OuterV1(new InnerV2(1)).toJSON();
expect(OuterV2.fromJSON(json)).toEqual(canonical());
});
it("loads OuterV2<InnerV1>", () => {
const json = new OuterV2(new InnerV1(1)).toJSON();
expect(OuterV2.fromJSON(json)).toEqual(canonical());
});
it("loads OuterV2<InnerV2>", () => {
const json = new OuterV2(new InnerV2(1)).toJSON();
expect(OuterV2.fromJSON(json)).toEqual(canonical());
});
});
});