diff --git a/src/__tests__/dates.test.js b/src/__tests__/dates.test.js new file mode 100644 index 0000000..56895e5 --- /dev/null +++ b/src/__tests__/dates.test.js @@ -0,0 +1,52 @@ +// @flow +/* + * MIT License + * + * Copyright (c) 2017 Uber Node.js + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import {ThriftFileConverter} from '../main/convert'; +// import {Thrift} from 'thriftrw'; + +test('thriftrw enums work in map constants', () => { + const fixturePath = 'src/__tests__/fixtures/dates.thrift'; + // const thrift = new Thrift({ + // entryPoint: fixturePath, + // allowFilesystemAccess: true, + // }); + const converter = new ThriftFileConverter(fixturePath, false); + expect(converter.generateFlowFile()).toMatchInlineSnapshot(` +"// @flow + +import thrift2flow$Long from \\"long\\"; + +export type UUID = string; + +export type URL = string; + +export type ISODate = string; + +export type TimestampMillis = number; + +export type Long = thrift2flow$Long; +" +`); +}); diff --git a/src/__tests__/fixtures/dates.thrift b/src/__tests__/fixtures/dates.thrift new file mode 100644 index 0000000..c033c9e --- /dev/null +++ b/src/__tests__/fixtures/dates.thrift @@ -0,0 +1,7 @@ +namespace java com.uber.types + +typedef string UUID +typedef string URL +typedef string (js.type = 'Date') ISODate +typedef i64 (js.type = 'Date') TimestampMillis +typedef i64 (js.type = 'Long') Long \ No newline at end of file diff --git a/src/__tests__/typedefs.test.js b/src/__tests__/typedefs.test.js index c9f20b5..a18d2c9 100644 --- a/src/__tests__/typedefs.test.js +++ b/src/__tests__/typedefs.test.js @@ -93,11 +93,11 @@ import type { function go(s : MyStruct) { const numbers : number[] = [s.f_MyByte, s.f_TransitiveTypedef, s.f_OtherStruct.num]; const structs : OtherStruct[] = [s.f_OtherStruct]; - const timestamps : Timestamp[] = ["string", s.f_OtherStruct.ts]; + const timestamps : Timestamp[] = [10, s.f_OtherStruct.ts]; return [numbers, structs, timestamps]; } - ` + `, }, r => { expect(r.errors.length).toBe(0); @@ -116,7 +116,7 @@ struct UserActivitiesRequest { 30: optional i64(js.type = "Long") fromTimestampNano 40: optional i64(js.type = "Long") toTimestampNano } -` +`, }; const root = tmp.dirSync().name; const paths = Object.keys(files); @@ -136,7 +136,7 @@ test('typedef long in global scope', () => { // language=thrift 'types.thrift': ` typedef i64 (js.type = "Long") Points -` +`, }; const root = tmp.dirSync().name; const paths = Object.keys(files); diff --git a/src/main/convert.js b/src/main/convert.js index 053f80c..c1df865 100644 --- a/src/main/convert.js +++ b/src/main/convert.js @@ -44,13 +44,13 @@ import type { ConstEntry, ConstMap, AstNode, - Definition + Definition, } from './ast-types'; const thriftOptions = { strict: false, allowFilesystemAccess: true, - allowOptionalArguments: true + allowOptionalArguments: true, }; function includeIdentifierOfFilename(filename: string): string { @@ -71,13 +71,13 @@ const primitives = { i64: 'Buffer', double: 'number', string: 'string', - void: 'void' + void: 'void', }; const i64Mappings = { Long: 'thrift2flow$Long', - Date: 'string', - Integer: 'number' + Date: 'number', + Integer: 'number', }; export class ThriftFileConverter { @@ -85,7 +85,7 @@ export class ThriftFileConverter { thrift: {| asts: {[filename: string]: Ast}, filename: string, - idls: {[filename: string]: {||}} + idls: {[filename: string]: {||}}, |}; withsource: boolean; ast: Ast; @@ -108,28 +108,38 @@ export class ThriftFileConverter { const includeIdentifier = includeIdentifierOfFilename(filename); return { filename: filename, - includePrefix: `${includeIdentifier}.` + includePrefix: `${includeIdentifier}.`, }; }) .concat([ { filename: this.thrift.filename, - includePrefix: '' - } + includePrefix: '', + }, ]) - .map(({filename, includePrefix}: {|filename: string, includePrefix: string|}) => { - this.thrift.asts[filename].definitions.forEach(definition => { - const identifier = `${includePrefix}${definition.id.name}`; - this.identifiersTable[identifier] = definition; - if (definition.type === 'Enum') { - definition.definitions.forEach(enumDefinition => { - this.identifiersTable[ - `${includePrefix}${definition.id.name}.${enumDefinition.id.name}` - ] = enumDefinition; - }); - } - }); - }); + .map( + ({ + filename, + includePrefix, + }: {| + filename: string, + includePrefix: string, + |}) => { + this.thrift.asts[filename].definitions.forEach(definition => { + const identifier = `${includePrefix}${definition.id.name}`; + this.identifiersTable[identifier] = definition; + if (definition.type === 'Enum') { + definition.definitions.forEach(enumDefinition => { + this.identifiersTable[ + `${includePrefix}${definition.id.name}.${ + enumDefinition.id.name + }` + ] = enumDefinition; + }); + } + }); + } + ); } generateFlowFile: () => string = () => { @@ -137,7 +147,7 @@ export class ThriftFileConverter { '// @flow', this.withsource && `// Source: ${this.thriftPath}`, this.generateImports(), - ...this.ast.definitions.map(this.convertDefinitionToCode) + ...this.ast.definitions.map(this.convertDefinitionToCode), ] .filter(Boolean) .join('\n\n'); @@ -162,13 +172,17 @@ export class ThriftFileConverter { return this.generateConst(def); default: throw new Error( - `Unknown definition type ${defType} found in ${path.basename(this.thriftPath)}` + `Unknown definition type ${defType} found in ${path.basename( + this.thriftPath + )}` ); } }; generateService = (def: Service) => - `export type ${def.id.name} = {\n${def.functions.map(this.generateFunction).join(',')}};`; + `export type ${def.id.name} = {\n${def.functions + .map(this.generateFunction) + .join(',')}};`; generateFunction = (fn: FunctionDefinition) => `${fn.id.name}: (${ @@ -190,8 +204,12 @@ export class ThriftFileConverter { }; generateEnum = (def: Enum, otherName?: string) => { - const values = def.definitions.map((d, index) => `'${d.id.name}': '${d.id.name}',`).join('\n'); - return `export const ${otherName !== undefined ? otherName : def.id.name}: $ReadOnly<{| + const values = def.definitions + .map((d, index) => `'${d.id.name}': '${d.id.name}',`) + .join('\n'); + return `export const ${ + otherName !== undefined ? otherName : def.id.name + }: $ReadOnly<{| ${values} |}> = Object.freeze({ ${values} @@ -207,10 +225,16 @@ export class ThriftFileConverter { .map((val: Identifier | Literal) => { if (val.type === 'Identifier') { if (val.name.includes('.')) { - const {definition} = this.definitionOfIdentifier(val.name, this.thrift.filename); + const {definition} = this.definitionOfIdentifier( + val.name, + this.thrift.filename + ); if (definition.type == 'EnumDefinition') { const scope = val.name.split('.')[0]; - const defAndFilename = this.definitionOfIdentifier(scope, this.thrift.filename); + const defAndFilename = this.definitionOfIdentifier( + scope, + this.thrift.filename + ); if (enumType === undefined && this.isEnum(defAndFilename)) { enumType = `${this.getIdentifier(scope, 'type')}[]`; } @@ -240,7 +264,9 @@ export class ThriftFileConverter { value !== undefined ) { const numValue = Number(value) > 0 ? Number(value) : -Number(value); - return `export const ${def.id.name}: ${String(numValue)} = ${String(numValue)};`; + return `export const ${def.id.name}: ${String(numValue)} = ${String( + numValue + )};`; } } if (value === undefined) { @@ -348,7 +374,9 @@ export class ThriftFileConverter { if (!fields.length) { return '{||}'; } - return fields.map((f: Field) => `{|${f.name}: ${this.convertType(f.valueType)}|}`).join(' | '); + return fields + .map((f: Field) => `{|${f.name}: ${this.convertType(f.valueType)}|}`) + .join(' | '); }; isOptional = (field: Field) => field.optional; @@ -357,7 +385,9 @@ export class ThriftFileConverter { const includes = this.ast.headers.filter(f => f.type === 'Include'); const relativePaths: Array = includes .map(i => path.parse(i.id)) - .map((parsed: {dir: string, name: string}) => path.join(parsed.dir, parsed.name)) + .map((parsed: {dir: string, name: string}) => + path.join(parsed.dir, parsed.name) + ) .map((p: string) => (p.startsWith('.') ? p : `./${p}`)); const generatedImports = relativePaths.map((relpath, index) => { let baseName = path.basename(relpath); @@ -415,9 +445,13 @@ export class ThriftFileConverter { if (definition) { return {definition, filename}; } - const [scope, name] = identifier.includes('.') ? identifier.split('.') : [null, identifier]; + const [scope, name] = identifier.includes('.') + ? identifier.split('.') + : [null, identifier]; if (scope === null) { - throw new Error(`local identifier ${identifier} missing in file ${filename}, name ${name}`); + throw new Error( + `local identifier ${identifier} missing in file ${filename}, name ${name}` + ); } const headerInclude = ast.headers .filter(f => f.type === 'Include') @@ -425,21 +459,37 @@ export class ThriftFileConverter { return path.basename(header.id, '.thrift') === `${scope}`; }); if (!headerInclude) { - throw new Error(`header include not found for scope ${scope} in filename ${filename}.`); + throw new Error( + `header include not found for scope ${scope} in filename ${filename}.` + ); } - const otherFilename = path.resolve(path.dirname(filename), headerInclude.id); + const otherFilename = path.resolve( + path.dirname(filename), + headerInclude.id + ); return this.definitionOfIdentifier(name, otherFilename); } /** * Follows typedef references to determine if a given identifier refers to an Enum. */ - isEnum({definition, filename}: {definition: AstNode, filename: string}): boolean { + isEnum({ + definition, + filename, + }: { + definition: AstNode, + filename: string, + }): boolean { if (definition.type === 'Enum') { return true; } - if (definition.type === 'Typedef' && definition.valueType.type === 'Identifier') { - return this.isEnum(this.definitionOfIdentifier(definition.valueType.name, filename)); + if ( + definition.type === 'Typedef' && + definition.valueType.type === 'Identifier' + ) { + return this.isEnum( + this.definitionOfIdentifier(definition.valueType.name, filename) + ); } return false; } @@ -447,10 +497,10 @@ export class ThriftFileConverter { getIdentifier = (identifier: string, kind: 'type' | 'value'): string => { // Enums can be referenced as either types and as values. For flow we need to slip // this up. - const {definition: def, filename: defFilename} = this.definitionOfIdentifier( - identifier, - this.thrift.filename - ); + const { + definition: def, + filename: defFilename, + } = this.definitionOfIdentifier(identifier, this.thrift.filename); if (kind === 'type') { if (this.isEnum({definition: def, filename: defFilename})) { return `$Values`; @@ -466,7 +516,11 @@ export class ThriftFileConverter { while (queue.length) { let [node, ...newQueue] = queue; queue = newQueue; - if (node.type === 'Struct' || node.type === 'Exception' || node.type === 'Union') { + if ( + node.type === 'Struct' || + node.type === 'Exception' || + node.type === 'Union' + ) { for (const field of node.fields) { queue = [...queue, field]; } @@ -503,7 +557,9 @@ export class ThriftFileConverter { // Enums are values, not types. To refer to the type, // we use $Values<...>. if (thriftValueType.type !== 'Identifier') { - throw new Error('Assertion failure. Enum reference has to be an identifier'); + throw new Error( + 'Assertion failure. Enum reference has to be an identifier' + ); } return `$Values`; } @@ -516,11 +572,15 @@ export class ThriftFileConverter { if (def.type === 'Const' && def.value.type === 'ConstMap') { const entries = def.value.entries.map(entry => { if (entry.key.type === 'Identifier') { - const identifierValue: AstNode = this.identifiersTable[entry.key.name]; + const identifierValue: AstNode = this.identifiersTable[ + entry.key.name + ]; if (identifierValue.type === 'EnumDefinition') { return `'${identifierValue.id.name}': ${valueType}`; } else { - throw new Error(`Unknown identifierValue type ${identifierValue.type}`); + throw new Error( + `Unknown identifierValue type ${identifierValue.type}` + ); } } else if (entry.key.type === 'Literal') { return `'${entry.key.value}': ${valueType}`;