From e3c43a65543a01f83892e0ddc5229d6f6b6d0421 Mon Sep 17 00:00:00 2001 From: Varun Ramesh Date: Fri, 28 Aug 2015 00:54:17 -0700 Subject: [PATCH] Service Framework 3.0 Diff #2 - TypeRegistry --- .../external-interfaces/1.0/jasmine.js | 1 + pkg/nuclide/server/.flowconfig | 2 + pkg/nuclide/server/package.json | 2 + .../service-parser/lib/TypeRegistry.js | 226 ++++++++++++++++++ .../service-parser/spec/TypeRegistry-spec.js | 157 ++++++++++++ 5 files changed, 388 insertions(+) create mode 100644 pkg/nuclide/service-parser/lib/TypeRegistry.js create mode 100644 pkg/nuclide/service-parser/spec/TypeRegistry-spec.js diff --git a/pkg/nuclide/external-interfaces/1.0/jasmine.js b/pkg/nuclide/external-interfaces/1.0/jasmine.js index df11c069d7..af5f0f9a2c 100644 --- a/pkg/nuclide/external-interfaces/1.0/jasmine.js +++ b/pkg/nuclide/external-interfaces/1.0/jasmine.js @@ -18,6 +18,7 @@ type JasmineMatcher = { toBeCloseTo(expected: number, precision: number): boolean; toBeDefined(): boolean; toBeFalsy(): boolean; + toBeTruthy(): boolean; toBeGreaterThan(expected: number): boolean; toBeLessThan(expected: number): boolean; toBeNull(): boolean; diff --git a/pkg/nuclide/server/.flowconfig b/pkg/nuclide/server/.flowconfig index b1c97f4d4b..87df6f11b9 100644 --- a/pkg/nuclide/server/.flowconfig +++ b/pkg/nuclide/server/.flowconfig @@ -3,6 +3,8 @@ [ignore] [libs] +./node_modules/nuclide-external-interfaces/1.0/jasmine.js +./node_modules/nuclide-service-parser/lib/types.js [options] diff --git a/pkg/nuclide/server/package.json b/pkg/nuclide/server/package.json index 057994f6eb..ece391b66d 100644 --- a/pkg/nuclide/server/package.json +++ b/pkg/nuclide/server/package.json @@ -32,6 +32,7 @@ "nuclide-path-search": "0.0.0", "nuclide-remote-search": "0.0.0", "nuclide-remote-uri": "0.0.0", + "nuclide-service-parser": "0.0.0", "nuclide-service-transformer": "0.0.0", "nuclide-source-control-helpers": "0.0.0", "nuclide-version": "0.0.0", @@ -43,6 +44,7 @@ "yargs": "3.1.0" }, "devDependencies": { + "nuclide-external-interfaces": "0.0.0", "nuclide-jasmine": "0.0.0", "rimraf": "2.2.8" }, diff --git a/pkg/nuclide/service-parser/lib/TypeRegistry.js b/pkg/nuclide/service-parser/lib/TypeRegistry.js new file mode 100644 index 0000000000..2cc9f2729b --- /dev/null +++ b/pkg/nuclide/service-parser/lib/TypeRegistry.js @@ -0,0 +1,226 @@ +'use babel'; +/* @flow */ + +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import assert from 'assert'; +import vm from 'vm'; + +import type {Type, NullableType, ArrayType, ObjectType} from './types'; + +/* + * This type represents a Transfomer function, which takes in a value, and either serializes + * or deserializes it. Transformer's are added to a registry and indexed by the name of + * the type they handle (eg: 'Date'). The second argument is the actual type object that represent + * the value. Parameterized types like Array, or Object can use this to recursively call other + * transformers. + */ +export type Transfomer = (value: any, type: Type) => Promise; + +/* + * The TypeRegistry is a centralized place to register functions that serialize and deserialize + * types. This allows for types defined in one service to include types from another service in + * another file. It also allows the ability to add new primitives, ranging from Buffer to NuclideUri + * that are not handled at the transport layer. The key concept is that marshalling functions can + * be recursive, calling other marshalling functions, ending at the primitives. + */ +export default class TypeRegistry { + /** Store marhsallers and and unmarshallers, index by the name of the type. */ + _marshallers: Map; + _unmarshallers: Map; + + constructor() { + this._marshallers = new Map(); + this._unmarshallers = new Map(); + + this._registerPrimitives(); + this._registerSpecialTypes(); + this._registerContainers(); + + // Register NullableType and NamedType + this.registerType('nullable', async (value: any, type: NullableType) => { + if (value === null || value === undefined) { + return null; + } + return await this.marshal(value, type.type); + }, async (value: any, type: NullableType) => { + if (value === null || value === undefined) { + return null; + } + return await this.unmarshal(value, type.type); + }); + + this.registerType('named', async (value: any, type: NamedType) => { + return await this.marshal(value, { kind: type.name }); + }, async (value: any, type: NullableType) => { + return await this.unmarshal(value, { kind: type.name }); + }); + } + + /** + * Register a type by providing both a marshaller and an unmarshaller. The marshaller + * will be called to transform the type before sending it out onto the network, while the + * unmarshaller will be called on values incoming from the network. + * @param typeName - The string name of the type that the provided marshaller / unmarshaller convert. + * @param marshaller - Serialize the type. + * @param unmarshaller - Deserialize the type. + */ + registerType(typeName: string, marshaller: Transformer, unmarshaller: Transformer): void { + if (this._marshallers.has(typeName) || this._unmarshallers.has(typeName)) { + throw new Error(`A type by the name ${typeName} has already been registered.`); + } + this._marshallers.set(typeName, marshaller); + this._unmarshallers.set(typeName, unmarshaller); + } + + /** + * Helper function for registering the marashaller/unmarshaller for a type alias. + * @param name - The name of the alias type. + * @param type - The type the the alias represents. + */ + registerAlias(name: string, type: Type): void { + this.registerType(name, value => this.marshal(value, type), + value => this.unmarshal(value, type)); + } + + /** + * Marshal an object using the appropriate marshal function. + * @param value - The value to be marshalled. + * @param type - The type object (used to find the appropriate function). + */ + marshal(value: any, type: Type): Promise { + return this._marshallers.get(type.kind)(value, type); + } + + /** + * Unmarshal and object using the appropriate unmarshal function. + * @param value - The value to be marshalled. + * @param type - The type object (used to find the appropriate function). + */ + unmarshal(value: any, type: Type): Promise { + return this._unmarshallers.get(type.kind)(value, type); + } + + _registerPrimitives(): void { + // Since string, number, and boolean are JSON primitives, + // they require no marshalling. Instead, simply create wrapped transformers + // that assert the type of their argument. + var stringTransformer = async arg => { + // Unbox argument. + arg = (arg instanceof String) ? arg.valueOf() : arg; + assert(typeof arg === 'string', 'Expected a string argument'); + return arg; + }; + var numberTransformer = async arg => { + // Unbox argument. + if (arg instanceof Number) { + arg = arg.valueOf(); + } + assert(typeof arg === 'number', 'Expected a number argument'); + return arg; + }; + var booleanTransformer = async arg => { + // Unbox argument + if (arg instanceof Boolean) { + arg = arg.valueOf(); + } + assert(typeof arg === 'boolean', 'Expected a boolean argument'); + return arg; + }; + + // Register these transformers + this.registerType('string', stringTransformer, stringTransformer); + this.registerType('number', numberTransformer, numberTransformer); + this.registerType('boolean', booleanTransformer, booleanTransformer); + } + + _registerSpecialTypes(): void { + // Serialize / Deserialize Javascript Date objects + this.registerType('Date', async date => { + assert(date instanceof Date, 'Expected date argument.'); + return date.toJSON(); + }, async dateStr => { + // Unbox argument. + dateStr = (dateStr instanceof String) ? dateStr.valueOf() : dateStr; + + assert(typeof dateStr === 'string', 'Expeceted a string argument.'); + return new Date(dateStr); + }); + + // Serialize / Deserialize RegExp objects + this.registerType('RegExp', async regexp => { + assert(regexp instanceof RegExp, 'Expected a RegExp object as an argument'); + return regexp.toString(); + }, async regStr => { + // Unbox argument. + regStr = (regStr instanceof String) ? regStr.valueOf() : regStr; + + assert(typeof regStr === 'string', 'Expected a string argument.'); + return vm.runInThisContext(regStr); + }); + + // Serialize / Deserialize Buffer objects through Base64 strings + this.registerType('Buffer', async buffer => { + assert(buffer instanceof Buffer, 'Expected a buffer argument.'); + return buffer.toString('base64'); + }, async base64string => { + // Unbox argument. + base64string = (base64string instanceof String) ? base64string.valueOf() : base64string; + + assert(typeof base64string === 'string', `Expected a base64 string. Not ${typeof base64string}`); + return new Buffer(base64string, 'base64'); + }); + } + + _registerContainers(): void { + // Serialize / Deserialize Arrays + this.registerType('array', async (value: any, type: ArrayType) => { + assert(value instanceof Array, 'Expected an object of type Array.'); + return await* value.map(elem => this.marshal(elem, type.type)); + }, async (value: any, type: ArrayType) => { + assert(value instanceof Array, 'Expected an object of type Array.'); + return await* value.map(elem => this.unmarshal(elem, type.type)); + }); + + // Serialize and Deserialize Objects + this.registerType('object', async (obj: any, type: ObjectType) => { + assert(typeof obj === 'object', 'Expected an argument of type object.'); + var newObj = {}; // Create a new object so we don't mutate the original one. + await* type.fields.map(async prop => { + // Check if the source object has this key. + if (obj.hasOwnProperty(prop.name)) { + newObj[prop.name] = await this.marshal(obj[prop.name], prop.type); + } else { + // If the property is optional, it's okay for it to be missing. + if (!prop.optional) { + throw new Error(`Source object is missing property ${prop.name}.`); + } + } + }); + return newObj; + }, async (obj: any, type: ObjectType) => { + assert(typeof obj === 'object', 'Expected an argument of type object.'); + var newObj = {}; // Create a new object so we don't mutate the original one. + await* type.fields.map(async prop => { + // Check if the source object has this key. + if (obj.hasOwnProperty(prop.name)) { + newObj[prop.name] = await this.unmarshal(obj[prop.name], prop.type); + } else { + // If the property is optional, it's okay for it to be missing. + if (!prop.optional) { + throw new Error(`Source object is missing property ${prop.name}.`); + } + } + }); + return newObj; + }); + + // TODO: Serialize Map and Set. + } +} diff --git a/pkg/nuclide/service-parser/spec/TypeRegistry-spec.js b/pkg/nuclide/service-parser/spec/TypeRegistry-spec.js new file mode 100644 index 0000000000..4656b45190 --- /dev/null +++ b/pkg/nuclide/service-parser/spec/TypeRegistry-spec.js @@ -0,0 +1,157 @@ +'use babel'; +/* @flow */ + +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import TypeRegistry from '../lib/TypeRegistry'; +import invariant from 'assert'; + +import type {NamedType, ArrayType, ObjectType} from '../lib/types'; + +describe('TypeRegistry', () => { + var typeRegistry; + beforeEach(() => { + typeRegistry = new TypeRegistry(); + }); + + it('Can serialize / deserialize basic primitive types', () => { + waitsForPromise(async () => { + invariant(typeRegistry); + + var expected = 'Hello World'; + var str = await typeRegistry.unmarshal(await typeRegistry.marshal(expected, {kind: 'string'}), {kind: 'string'}); + expect(str).toBe(expected); + + var expected = 312213; + var num = await typeRegistry.unmarshal(await typeRegistry.marshal(expected, {kind: 'number'}), + {kind: 'number'}); + expect(num).toBe(expected); + + var expected = false; + var bool = await typeRegistry.unmarshal(await typeRegistry.marshal(expected, {kind: 'boolean'}), + {kind: 'boolean'}); + expect(bool).toBe(expected); + }); + }); + + it('Can serialize / deserialize complex types like Date, Regex and Buffer', () => { + waitsForPromise(async () => { + invariant(typeRegistry); + + var expected = new Date(); + var type: NamedType = {kind: 'named', name: 'Date'}; + var date = await typeRegistry.unmarshal(await typeRegistry.marshal(expected, type), type); + expect(date.getTime()).toBe(expected.getTime()); + + var expected = /nuclide/ig; + var type: NamedType = {kind: 'named', name: 'RegExp'}; + var regex = await typeRegistry.unmarshal(await typeRegistry.marshal(expected, type), type); + expect(regex.source).toBe(expected.source); + + var expected = new Buffer('test buffer data.'); + var type: NamedType = {kind: 'named', name: 'Buffer'}; + var buf: Buffer = await typeRegistry.unmarshal(await typeRegistry.marshal(expected, type), type); + expect(expected.equals(buf)).toBeTruthy(); + }); + }); + + it('Can serialize / deserialize parameterized types like Array and Object', () => { + waitsForPromise(async () => { + invariant(typeRegistry); + + // An array of buffers. + var expected = [new Buffer('testdas'), new Buffer('test')]; + var type: ArrayType = { + kind: 'array', + type: { + kind: 'named', + name: 'Buffer', + }, + }; + var result = await typeRegistry.unmarshal(await typeRegistry.marshal(expected, type), type); + expect(result.length).toBe(2); + expect(result[0].equals(expected[0])).toBeTruthy(); + expect(result[1].equals(expected[1])).toBeTruthy(); + + // Object with a a nullable property and a buffer property. + var type: ObjectType = { + kind: 'object', + fields: [ + // A nullable string property. + { + type: { kind: 'nullable', type: { kind: 'string' } }, + name: 'a', + optional: false, + }, + // A non-nullable buffer property. + { + type: { kind: 'Buffer' }, + name: 'b', + optional: false, + }, + // An optional number property. + { + type: { kind: 'number' }, + name: 'c', + optional: true, + }, + ], + }; + var expected = { a: null, b: new Buffer('test') }; + var result = await typeRegistry.unmarshal(await typeRegistry.marshal(expected, type), type); + expect(result.a).toBeNull(); + expect(result.b.equals(expected.b)).toBeTruthy(); + }); + }); + + it('Can serialize / deserialize type aliases.', () => { + waitsForPromise(async () => { + invariant(typeRegistry); + typeRegistry.registerAlias('ValueTypeA', ValueTypeA); + + var data = {valueA: 'Hello World.', valueB: null}; + var type: NamedType = {kind: 'named', name: 'ValueTypeA'}; + var result = await typeRegistry.unmarshal(await typeRegistry.marshal(data, type), type); + expect(result.valueA).toBe(data.valueA); + expect(result.valueB).toBeNull(); + expect(result.hasOwnProperty('valueC')).toBeFalsy(); + }); + }); +}); + +var ValueTypeA = { + fields: [ + { + name: 'valueA', + optional: false, + type: { + kind: 'string', + }, + }, + { + name: 'valueB', + optional: false, + type: { + kind: 'nullable', + type: { + kind: 'named', + name: 'ValueTypeB', + }, + }, + }, + { + name: 'valueC', + optional: true, + type: { + kind: 'boolean', + }, + }, + ], + kind: 'object', +};