Giter Club home page Giter Club logo

thrift-typescript's Introduction

Thrift TypeScript

Generate TypeScript from Thrift IDL files.

Installation

$ npm install --save @creditkarma/thrift-typescript

Usage

Thrift TypeScript provides both a JavaScript and a command line API.

Given the following files

thrift/simple.thrift

struct MyStruct {
    1: required i32 id,
    2: required bool field1,
    # 3: required string field,
    4: required i16 field,
}

You can generate TypeScript via the command line:

$ thrift-typescript --target apache --rootDir . --sourceDir thrift --outDir codegen simple.thrift

The available options are:

  • --rootDir: This is used to resolve out and source directories. Defaults to current directory.
  • --outDir: The directory to save generated files to. Will be created if it doesn't exist. Defaults to 'codegen'.
  • --sourceDir: The directory to search for source Thrift files. Defaults to 'thrift'.
  • --target: The core library to generate for, either 'apache' or 'thrift-server'. Defaults to 'apache'.
  • --strictUnions: Should we generate strict unions (Only available for target = 'thrift-server'. More on this below). Defaults to undefined.
  • --fallbackNamespace: The namespace to fallback to if no 'js' namespace exists. Defaults to 'java'. Set to 'none' to use no namespace.
  • --withNameField: Should we generate a __name field on each struct-like object that contains its Thrift name (Only available for target = 'thrift-server'). Defaults to undefined.

All other fields are assumed to be source files.

If no explicit list of files is provided all files ending in '.thrift' found in the sourceDir will be used.

You can gen code from more than one Thrift file:

$ thrift-typescript one.thrift two.thrift three.thrift

JavaScript API

You can also generate files using the JavaScript API:

import { generate } from '@creditkarma/thrift-typescript'

// Generates TypeScript and saves to given outDir
generate({
    rootDir: '.',
    sourceDir: 'thirft',
    outDir: 'codegen',
    target: 'thrift-server',
    files: [
        'simple.thrift'
    ],
    fallbackNamespace: 'java',
})

Thrift to String

You can also generate TypeScript from a string of Thrift without saving to file.

Note: This method of code generation does not support includes. The Thrift generator must be able to resolve all identifiers which it can't do without a description of the file structure.

import { readFileSync } from 'fs'
import { make } from '@creditkarma/thrift-typescript'

const rawThrift: string = readFileSync('./thrift/simple.thrift', 'utf-8')
const generatedCode: string = make(rawThrift)

Thrift Server

v2.x of Thrift TypeScript equires @creditkarma/thrift-server v0.7.0 or higher

While Thrift TypeScript can be used to generate code comaptible with the Apache Thrift Library, it is recommended to use with Thrift Server. Details on the Apache usage are below.

Thrift Server adds Thrift support to Express or Hapi with plugins or middleware. The other advantange of using the codegen with Thrift Server is the addition of context to service clients and service handlers. Context can be used to do things like auth or tracing in Thrift service methods. Context is an optional final parameter to all service handler methods and all service client methods.

Install the Thrift Server implementation for your server of choice. For this example we will be using express middleware and the request http client library.

$ npm install --save @creditkarma/thrift-server-core
$ npm install --save @creditkarma/thrift-server-express
$ npm install --save @creditkarma/thrift-client
$ npm install --save express
$ npm install --save request
$ npm install --save @types/express
$ npm install --save @types/request

Given this service let's build a client and server based on our generated code.

service Caluculator {
    i32 add(1: i32 left, 2: i32 right)
    i32 subtract(1: i32 left, 2: i32 right)
}

Run codegen for your Thrift service. The target option is required here, otherwise the generated code will only work with the Apache libs.

$ thrift-typescript --target thrift-server --rootDir . --sourceDir thrift --outDir codegen

Client

In this example we are using the Request library as our underlying connection instance. The options for Request (CoreOptions) are our request context.

You'll notice that the Client class is a generic. The type parameter represents the type of the context. This is usually going to be of type CoreOptions from the Request library.

import {
    createHttpClient,
    HttpConnection,
} from '@creditkarma/thrift-client'

import * as request from 'request'
import { CoreOptions } from 'request'

import { Calculator } from './codegen/calculator'

const CONFIG = {
    hostName: 'localhost',
    port: 8045
}

const thriftClient: Calculator.Client<CoreOptions> = createHttpClient(Calculator.Client, CONFIG)

thriftClient.add(5, 7, { headers: { 'X-Trace-Id': 'xxxxxx' } })
    .then((response: number) => {
        expect(response).to.equal(12)
        done()
    })

Server

In the server we can then inspect the headers we set in the client.

import * as bodyParser from 'body-parser'
import * as express from 'express'
import { ThriftServerExpress } from '@creditkarma/thrift-server-express'

import {
    Calculator,
} from './codegen/calculator'

// express.Request is the context for each of the service handlers
const serviceHandlers: Calculator.IHandler<express.Request> = {
    add(left: number, right: number, context?: express.Request): number {
        if (context && context.headers['x-trace-id']) {
            // You can trace this request, perform auth, or use additional middleware to handle that.
        }
        return left + right
    },
    subtract(left: number, right: number, context?: express.Request): number {
        return left - right;
    },
}

const PORT = 8090

const app = express()

app.use(
    '/thrift',
    bodyParser.raw(),
    ThriftServerExpress(Calculator.Processor, serviceHandlers),
)

app.listen(PORT, () => {
    console.log(`Express server listening on port: ${PORT}`)
})

Generated Data Types

When generating TypeScript from Thrift source what data types are generated?

Simple Types

These are: booleans, strings, numbers, sets, maps, lists, enums and typedefs. All of these translate almost directly to TypeScript.

Given Thrift:

const bool FALSE_CONST = false
const i32 INT_32 = 32
const i64 INT_64 = 64
const list<string> LIST_CONST = ['hello', 'world', 'foo', 'bar']
const set<string> SET_CONST = ['hello', 'world', 'foo', 'bar']
const map<string,string> MAP_CONST = { 'hello': 'world', 'foo': 'bar' }

enum Colors {
    RED,
    GREEN,
    BLUE,
}

typedef string name

Generated TypeScript:

export const FALSE_CONST: boolean = false;
export const INT_32: number = 32;
export const INT_64: thrift.Int64 = new thrift.Int64(64);
export const LIST_CONST: Array<string> = ["hello", "world", "foo", "bar"];
export const SET_CONST: Set<string> = new Set(["hello", "world", "foo", "bar"]);
export const MAP_CONST: Map<string, string> = new Map([["hello", "world"], ["foo", "bar"]]);

export enum Colors {
    RED,
    GREEN,
    BLUE
}

export type name = string;

The only interesting thing here is the handling of i64. JavaScript doesn't support a full 64-bits of integer percision, so we wrap the value in an Int64 object. You will notice that this doesn't really help in cases where you define a constant or default value in your Thrift file, but it does allow 64-bit integers received from outside of JS to be handled correctly. The object is exported from @creditkarma/thrift-server-core and extends node-int64.

Struct

A struct is intuitively analogous to an interface.

Given Thrift:

struct User {
    1: required string name
    2: string email
    3: required i32 id
}

Generated TypeScript:

export interface IUser {
    name: string
    email?: string
    id: number
}

Note: We adopt the convention of prefixing interfaces names with a capital 'I'.

Only fields that are explicitly required loose the ?.

The __name Field

You can pass an option to generate an additional property on each struct-like (structs, unions, exceptions) object that is its literal name in the Thrift file.

For example if we rendered the User struct with the --withNameField option the generated TypeScript would change:

export interface IUser {
    __name: "User"
    name: string
    email?: string
    id: number
}

When generating with this option any data types you create and pass into a client method do not need the __name field. However, any data you get back from a service will contain this additional property.

Union

Unions in Thrift are very similar to structs. The difference is they only allow one field to be set. They also require that one field is set. Implicitly all fields are optional, but one field must be set.

So, this translates into a struct with all optional fields:

Given Thrift:

union MyUnion {
    1: string option1
    2: i32 option2
}

Generated TypeScript (without strict unions):

export interface IMyUnion = {
    option1?: string
    option2?: undefined
}

Note: The difference here is that a runtime error will be raised if one of the fields isn't set or if more than one of the fields is set.

Exception

Exceptions are errors that can be thrown by service methods. It is more natural in JS/TS to create and throw new errors. So our defined exceptions will become JS classes.

Given Thrift:

exception MyException {
    1: string message
    2: i32 code
}

Generated TypeScript:

export class MyException extends thrift.StructLike implements IMyException {
    public message: string
    public code?: number
    constructor(args?: { message?: string, code?: number }) {
        // ...
    }
}

Then in your service client you could just throw the exception as you would any JS error throw new MyException({ message: 'whoops', code: 500 });

Service

Services are a little more complex. There are two parts to a service. There is the Client for sending service requests and the Processor for handling service requests. The service Client and the service Processor are each generated classes. They are wrapped, along with some other internal objects, in a namespace that has the name of your service.

Given Thrift:

service MyService {
    User getUser(1: i32 id) throws (1: MyException exp);
}

Generated TypeScript:

export namespace MyService {
    export class Client<Context> {
        constructor(connection: thrift.IThriftConnection<Context>) {
            // ...
        }
        getUser(id: number): Promise<User> {
            // ...
        }
    }
    export interface IHandler<Context> {
        getUser(id: number, context?: Context): User | Promise<User>
    }
    export class Processor {
        constructor(handler: IHandler<Context>) {
            // ...
        }
        public process(input: thrift.TProtocol, output: thrift.TProtocol, context: Context): Promise<Buffer> {
            // ...
        }
    }
}

The Client is pretty straight forward. You create a Client instance and you can call service methods on it. The inner-workings of the Processor aren't something consumers need to concern themselves with. The more interesting bit is IHandler. This is the interface that service teams need to implement in order to meet the promises of their service contract. Create an object that satisfies <service-name>.IHandler and pass it to the construction of <service-name>.Processor and everything else is handled for you.

Loose Types

Given these two structs:

struct User {
    1: required i64 id
}

struct Profile {
    1: required User user
    2: binary data
    3: i64 lastModified
}

There is something of a difference between how we want to handle things in TypeScript and how data is going to be sent over the wire. Because of this when we generate interfaces for these structs we generate two interfaces for each struct, one is an exact representation of the Thrift, the other is something looser that provides more flexibility to working with the data in JavaScript.

The main difference is that fields marked as i64 can be represented as a number, as string or an Int64 object and binary can be represented as either a string or a Buffer object.

JavaScript traditionally (bigint is new and not fully supported yet) does not support 64-bit integers. This means we need to wrap the Thrift i64 type in the Int64 object to maintain precision. In your TypeScript code you may be working with these just as number (confident JavaScript's 53-bit precision is good enough for you) or as a string. These loose types allow you to do that and the generated code will handle the conversions to Int64 for you.

Generated TypeScript:

interface IUser {
    id: thrift.Int64
}
interface IUserArgs {
    id: number | string | thrift.Int64
}
interface IProfile {
    user: IUser
    data?: Buffer
    lastModified?: thrift.Int64
}
interface IProfileArgs {
    user: IUserArgs
    data?: string | Buffer
    lastModified?: number | string | thrift.Int64
}

The names of loose interfaces just append Args onto the end of the interface name. The reason for this is these interfaces will most often be used as arguments in your code.

Where are the loose interfaces used? The loose interfaces can be used anywhere you, the application developer, are giving data to the generated code, either as the arguments to a client method or the return value of a service handler.

If we had this service:

service ProfileService {
    Profile getProfileForUser(1: User user)
    User getUser(1: i64 id)
}

And generated TypeScript:

namespace ProfileService {
    export class Client<Context> {
        constructor(connection: thrift.IThriftConnection<Context>) {
            // ...
        }
        getProfileForUser(user: IUserArgs, context?: Context): Promise<IProfile> {
            // ...
        }
        getUser(id: number | string | Int64): Promise<IUser> {
            // ...
        }
    }
    export interface IHandler<Context> {
        getProfileForUser(user: IUser, context: Context): Promise<IProfileArgs>
        getUser(id: Int64, context: Context): Promise<IUserArgs>
    }
}

As you can see from this sketch of generated types when data leave application code and crossed the boundary into the generated code you can pass loose values, when the data comes from generated code it will always be of the strict types.

We can use a User object where the id is a number or a string without having to wrap it in Int64. These conversions are handled for us. A number passed in is wrapped in Int64 by using the Int64 constructor: new Int64(64). A string passed in place of an Int64 is converted using the static fromDecimalString method: Int64.fromDecimalString('64'). Similarly string data can be passed to a binary field and the conversion to Buffer is handled under the hood. This are just convinience interfaces to make handling the Thrift objects in TypeScript a little easier. You will notice service methods always return an object of the more strict interface. Also, the more strict interface can always be passed where the loose interface is expected.

Sending Data Over the Wire

When it comes to struct-like data types (struct, union and exception) usually you don't need to know much more than what data types are generated. However, in addition to the generated interface/union/class the code generator also creates a companion object that knows how to send the given object over the wire.

Looking back at the User object from our struct example, in addition to the interface, the code generator creates a codec object like this:

export const UserCodec: thrift.IStructCodec<IUserArgs, IUser> {
    encode(obj: IUserArgs, output: thrift.TProtocol): void {
        // ...
    },
    decode(input: thrift.TProtocol): IUser {
        // ...
    }
}

It's just an object that knows how to read the given object from a Thrift Protocol or write the given object to a Thrift Protocol.

The codec will always follow this naming convention, just appending Codec onto the end of your struct name.

Strict Unions

Note: Strict unions require thrift-server version 0.13.x or higher.

This is an option only available when generating for thrift-server. This option will generate Thrift unions as TypeScript unions. This changes the codegen in a few significant ways.

Back with our example union definition:

union MyUnion {
    1: string option1
    2: i32 option2
}

When compiling with the --strictUnions flag we now generate TypeScript like this:

enum MyUnionType {
    MyUnionWithOption1 = "option1",
    MyUnionWithOption2 = "option2"
}
type MyUnion = IMyUnionWithOption1 | IMyUnionWithOption2
interface IMyUnionWithOption1 {
    __type: MyUnionType.MyUnionWithOption1
    option1: string
    option2?: undefined
}
interface IMyUnionWithOption2 {
    __type: MyUnionType.MyUnionWithOption2
    option1?: undefined
    option2: number
}
type MyUnionArgs = IMyUnionWithOption1Args | IMyUnionWithOption2Args
interface IMyUnionWithOption1Args {
    option1: string
    option2?: undefined
}
interface IMyUnionWithOption2Args {
    option1?: undefined
    option2: number
}

The enum represents all potential values of the __type property attached to each variation of our union. Instead of generating one interface with optional properties we generate one interface for each field where that field is required. Our resulting type is then the union of multiple interfaces each with only one property. This provides compile-time guarantees that we are setting one and only one field for the union.

The loose interfaces, the Args interfaces, behave much like the loose interfaces for structs. They allow you to use number in place of Int64 or allow you to pass either string or Buffer for binary types. In addition, they also forgo the __type property. In the codegen we can tell what you are passing by the fields you set. This means in most instances you don't need to provide the __type property. You can use the loose interfaces as the return value for service functions or as the arguments for client methods.

This output is more complex, but it allows us to do a number of things. The most significant of which may be that it allows us to take advantage of discriminated unions in our application code:

function processUnion(union: MyUnion) {
    switch (union.__type) {
        case MyUnionType.MyUnionWithOption1:
            // Do something
        case MyUnionType.MyUnionWithOption2:
            // Do something
        default:
            const _exhaustiveCheck: never = union
            throw new Error(`Non-exhaustive match for type: ${_exhaustiveCheck}`)
    }
}

The fact that each interface we generate defines one required field and some n number of optional undefined fields we can do things like check union.option2 !== undefined without a compiler error, but we will get a compiler error if you try to use a value that shouldn't exist on a given union. This expands the ways you can operate on unions to be more general.

Using this form will require that you prove to the compiler that one (and only one) field is set for your unions.

In addition to the changed types output, the --strictUnions flag changes the output of the Codec object. The Codec object will have one additional method create. The create method takes one of the loose interfaces and coerces it into the strict interface (including the __type property).

For the example MyUnion that would be defined as:

const MyUnionCodec: thrift.IStructToolkit<IUserArgs, IUser> { = {
    create(args: MyUnionArgs): MyUnion {
        // ...
    },
    encode(obj: IUserArgs, output: thrift.TProtocol): void {
        // ...
    },
    decode(input: thrift.TProtocol): IUser {
        // ...
    }
}

Note: In a future breaking release all the Codec objects will be renamed to Toolkit as they will provide more utilities for working with defined Thrift objects.

Apache Thrift

The generated code can also work with the Apache Thrift Library.

$ npm install --save thrift
$ npm install --save @types/thrift

Given this service let's build a client and server based on our generated code.

service Calculator {
    i32 add(1: i32 left, 2: i32 right)
    i32 subtract(1: i32 left, 2: i32 right)
}

Run codegen for your Thrift service. Here the --target option isn't needed as apache is the default build target.

$ thrift-typescript --rootDir . --sourceDir thrift --outDir codegen

Client

import {
    createHttpConnection,
    createHttpClient,
    HttpConnection,
} from 'thrift'

import { Calculator } from './codegen/calculator'

// The location of the server endpoint
const CONFIG = {
    hostName: 'localhost',
    port: 8045
}

const options = {
    transport: TBufferedTransport,
    protocol: TBinaryProtocol,
    https: false,
    headers: {
        Host: config.hostName,
    }
}

const connection: HttpConnection = createHttpConnection(CONFIG.hostName, CONFIG.port, options)
const thriftClient: Calculator.Client = createHttpClient(Calculator.Client, connection)

// All client methods return a Promise of the expected result.
thriftClient.add(5, 6).then((result: number) =>{
    console.log(`result: ${result}`)
})

Server

import {
    createWebServer,
    TBinaryProtocol,
    TBufferedTransport,
} from 'thrift'

import { Calculator } from './codegen/calculator'

// Handler: Implement the Calculator service
const myServiceHandler = {
    add(left: number, right: number): number {
        return left + right
    },
    subtract(left: number, right: number): number {
        return left - right
    },
}

// ServiceOptions: The I/O stack for the service
const myServiceOpts = {
    handler: myServiceHandler,
    processor: Calculator,
    protocol: TBinaryProtocol,
    transport: TBufferedTransport
}

// ServerOptions: Define server features
const serverOpt = {
    services: {
        '/': myServiceOpts
    }
}

// Create and start the web server
const port: number = 8045;
createWebServer(serverOpt).listen(port, () => {
    console.log(`Thrift server listening on port ${port}`)
})

Notes

The gererated code can be used with many of the more strict tsc compiler options.

{
    "compilerOptions": {
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "noUnusedLocals": true
    }
}

Development

Install dependencies with

npm install

Build

npm run build

Run test in watch mode

npm run test:watch

Contributing

For more information about contributing new features and bug fixes, see our Contribution Guidelines. External contributors must sign Contributor License Agreement (CLA)

License

This project is licensed under Apache License Version 2.0

thrift-typescript's People

Contributors

csordasmarton avatar hayes avatar ian-harshman-ck avatar kevin-greene-ck avatar kevinbgreene avatar ksonnad avatar nnance avatar phated avatar ramakrishna-battala-ck avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

thrift-typescript's Issues

types.hbs doesn't support containers

Currently breaking due to being unable to call .toUpperCase on the type because it is an object but they have much different logic so it needs to get implemented.

Separate normalization/validation pass

I'm thinking their should be some sort of normalization and validation done before the code generation. The reason I think this is beneficial is to resolve custom types and find out if anything used wasn't declared in the definition.

If this makes sense to have, should both be done in a single pass or should it be split into 2 separate phases?

Add version info to generated source

We should add a docblock header to the top of each source file so we can trace generated source back to a particular version of this tool.

Something like:

/**
 * Generated by @creditkarma/thrift-typescript
 * Version 0.0.2
 */

Only support npm version of thrift library

While reviewing container types, I've found some inconsistencies between the thrift.js clientside library and the thrift library on npm. Would it be better to output code that utilizes the npm version of the library?

Allocation failed - JavaScript heap out of memory

For thrift files whose dependency tree (the file and all of the includes) can be large node can sometimes run out of memory while running the generator.

The solution for this is to build a file, save it and deallocate as much data as possible before moving on to the next file. Currently we are keeping all of the file's data after the file has been rendered, keeping all data until the entire dependency tree has been rendered.

A temporary solution for this is to increase the memory allocated for node when running the generator:

$ node --max_old_space_size=8192 dist/main/bin/index.js <options>

Add support for a Thrfit Struct interface

I some use cases we need to have a generic Thrift Struct Interface defined in typings and added to the code generator so we can identify something as a Thrift Struct.

Need to resolve `include` paths

Once this syntax is implemented in thrift-parser, this module will need to resolve them.

A few decisions to make:

  • Should it read the includes by default or only when an option is set (-r flag in the traditional thrift parser)?
  • Should separate output files be generated or combined into one? Ref #14
  • Should thrift-typescript or thrift-parser be responsible for resolving the location of the file? Currently, I'm resolving in thrift-parser but that might be wrong. This one has been answered by upstream; it seems that thrift-typescript will be doing the resolution.

Typesafe Struct Constructors

This is something that stems from the original Apache implementation, but I think it could be improved upon.

In the generated code, thrift structs are currently populated via the read instance method:

const struct = new MyStruct()
struct.read(inputProtocol)

This means that the MyStruct's constructor arg has to be optional, otherwise we wouldn't be able to call the empty constructor. Unfortunately this makes constructors less type-safe for consumers. Even if MyStruct has a bunch of required fields...

struct MyStruct {
  1: required i32 field1
  2: required string field2
   ...

... the constructor can still be called with no arguments because the generated code requires it.

const struct = new MyStruct() // compiles, but should require fields

Alternative Approach

The read methods can be static. Something like

const struct = MyStruct.read(input)

This would free up the constructor signature to require a valid set of input arguments, which would result in required fields being type-checked (If a struct has no fields, we could make an exception in that case).

class MyStruct {
    constructor(args: IMyStructArgs) {...}  // constructor args are no longer optional
}

I've POC'd this by manually editing generated code and it seems to work just fine, though I haven't looked at memory / time complexity.

Anyway, not a high priority, just some food for thought.

Default negative numbers can't be parsed

if i have something like this:

struct SomeStruct {
  1: optional i16 bpm = 0
  2: optional byte confidence = -1
}

The parser complains:

Parse Failure: 1 errors found:


Parse Error:

Message: FieldType expected but found: IntegerLiteral

174 |   2: optional byte confidence = -1
                                      ^

/usr/local/lib/node_modules/@creditkarma/thrift-typescript/dist/main/utils.js:25
            throw new Error('Unable to parse source');

Prefer handlebars templates over AST?

When thinking about this effort, I had envisioned transforming the Thrift AST generated by thrift-parser to a JS AST and then use a tool to output the generated JS. I see that a lot of work has been done with handlerbars templates and I'm not sure it's worth it to move away from that. @nnance any thoughts?

Generate a single file with exported namespaces

The thrift binary outputs a bunch of different files (one *_types.js per thrift file and one file per service). Should we generate a single file instead? It seems like it would be easier from an import/usage perspective.

How should we test the Node abstraction?

Should we test the result of toAST() is a specific AST shape or should we pass the result into a printer and compare the output text string?

I'm open to other options also.

Should aliases that are resolved to a base type be eliminated from output?

The way we are currently resolving:

struct Embed {};

typedef Embed myEmbed;

struct MyStruct {
    1: myEmbed embedded
}

is to:

export type myEmbed = Embed;

export class Embed {};

export class MyStruct {
    ...
    embedded?: Embed; // myEmbed was resolved to Embed
    ...
}

This is resolving the alias to an alias down to the simplest reference. Do we want to keep the myEmbed type alias in the generated output when this happens?

Target node/ES2015 only

A lot of my open questions seem to be around features that would make things much simpler (Promises, Maps, etc). What environments are we targeting?

Remove `success` property on Struct generics

I noticed in the proof-of-concept that a success boolean property was being added to structs but it doesn't seem like the thrift binary does that. What is that property used for?

Type check assignment of default field values

The parser does not guarantee that default values are assigned properly. For example you could give a map<i32,string> the default value of 'foobar'. We should validate these assignments and give the client a useful error.

demo has something wrong

service Caluculator {
i32 add(1: i32 left, 2: i32 right)
i32 subtract(1: i32 left, 2: i32 right)
}

import { Calculator } from './codegen/calculator'
two word type error

Allow * for language choice in namespace declarations

namespace * MyNamespace

is recommended in https://www.manning.com/books/programmers-guide-to-apache-thrift-cx (please ignore the cancelled message) and compiles fine using the namespace in all languages (that I've tried) using the Apache Thrift code generator.

With thrift-typescript I encounter:

    Parse Failure: 2 errors found:


    Scan Error:

    Message: Unexpected token: *

    4 | namespace * MyNamespace
                  ^


    Parse Error:

    Message: Unable to find name identifier for namespace

    4 | namespace * MyNamespace

I can bypass it by replacing * with js but then I am going to have to duplicate the namespace line for each language I want to support.

Is this an approach that thrift-typescript could support or is my current approach actually a bad idea in practice?

Incorrect switch case fall-through with "--target thrift-server"

In the genrated Processor code, the process() function looks like the following:

public process(input: thrift.TProtocol, output: thrift.TProtocol, context: Context): Promise<Buffer> {
            return new Promise<Buffer>((resolve, reject): void => {
                const metadata: thrift.IThriftMessage = input.readMessageBegin();
                const fieldName: string = metadata.fieldName;
                const requestId: number = metadata.requestId;
                const methodName: string = "process_" + fieldName;
                switch (methodName) {
                    case "process_foo": {
                        resolve(this.process_foo(requestId, input, output, context));
                    }
                    case "process_bar": {
                        resolve(this.process_bar(requestId, input, output, context));
                    }
                    default: {
                        input.skip(thrift.TType.STRUCT);
                        input.readMessageEnd();
                        const errMessage = "Unknown function " + fieldName;
                        const err = new thrift.TApplicationException(thrift.TApplicationExceptionType.UNKNOWN_METHOD, errMessage);
                        output.writeMessageBegin(fieldName, thrift.MessageType.EXCEPTION, requestId);
                        thrift.TApplicationExceptionCodec.encode(err, output);
                        output.writeMessageEnd();
                        resolve(output.flush());
                    }
                }
            });

You can notice there is no "break;" sentence in the case clauses.
This causes all the handler functions to actually be called, as long as they have no arguments (if they do, argument parsing fails, and the function is not called).

The result is that having functions without arguments in a service is nearly unusable.

Should service generation be done before new Thrift client library?

@nnance While trying to understand the way node's thrift client library does send/receive for services, I found that it does some really hacky stuff (they even have a TODO to fix it) that would likely be going away. Is it worth the effort to do code generation that interops with their current implementation?

Investigate typings for thirft-parser IDL

Someone submitted typings to the thrift-parser project but I'm not sure how thorough they are. I need to investigate and use them where appropriate (or fix if they are missing things).

Utilize a sub-namespace of js.ts

We could either use the standard js namespace or maybe a ts namespace? I guess we could use a js.ts or js.typescript if we wanted to be verbose.

Enums in TypeScript are very lax

In researching enum in TypeScript, I found that it is a subtype of number, which makes it very lax during type checking.

For example, the following is valid TypeScript:

enum Operation {
  ADD, SUBTRACT
}

const op: Operation = 4; // Invalid enum value

Should we use the enum type to generate thrift enum definition or should we do something else?

Use Map data type to store Map container data

I found a bug with the output in the JS that the thrift binary outputs.

Sample:

map<bool, string> = {false: 'test'}

When this is serialized, it will become {true: 'test'} on the server because false is resolved as a string and checked for truthiness.

Add a debugger similar to thrift-parser

For the static analysis phases (resolver, validator) we should report errors similar to how it is being done in the parser. Don't throw on first error, collect errors and print errors with the line of source that caused the error, highlighting the problem token.

there are some wrong when create dir in windows

function mkdir(dirPath) {
console.log("dirPath:" + dirPath + " " + path.sep);

var parts = dirPath.split(path.sep).filter((val) => val !== '');

// i add this code it can run in windows
if (os.platform() === 'win32') {
parts = parts.slice(1)
}
if (parts.length > 0 && path.isAbsolute(dirPath)) {
createPath(parts, rootDir());
}
else if (parts.length > 0) {
createPath(parts, process.cwd());
}
}

Services cannot return promises

I'll use this simple service for reference.

service MyService {
   Response call(1: Request request)
}

In this case, the tool generates something like

export interface IMyServiceHandler<Context> {
    call: (request: Request, context: Context) => Response;
}

Notice that the return type does not allow for promises to be returned. I believe we can make the return type Response | Promise<Response> as a non-breaking change.

Enum aliases can't be used

It seems that it's possible to alias an enum in typescript but it can't be referenced from outside the module.

e.g.

export namespace MyThing {
    export type enumAlias = MyEnum;
    export type anotherAlias = MyEnum;
    export enum MyEnum {
        ADD,
        SUBTRACT,
        MULTIPLY,
        DIVIDE
    }
}

MyThing.enumAlias.ADD; // [ts] Property 'enumAlias' does not exist on type 'typeof MyThing'.

What should we do? Ref #30

Always return a promise from service methods

TypeScript supports Promises but doesn't shim them; however, it does shim async/await. This would require a shim library to be loaded (I believe the thrift library already requires & exports the Q promise library).

Needs to have @types/thrift installed

While implementing Exceptions, I found that TypeScript doesn't like extending with JS constructors (such as Thrift.TException) but by installing @types/thrift, that constructor is declared as a proper class and TypeScript stops complaining.

How should we handle needing this dependency installed?

Do we want to push some assumptions in the output?

I am wondering if we should generate the output with assumptions about how the CK team uses the client library.

The best example I found so far is:

// The Thrift tutorial teaches Struct usage like:
var work = new Work();
work.op = tutorial.Operation.DIVIDE;
work.num1 = 1;
work.num2 = 0;

// But the Constructor can (and must for required) take the properties upon construction
var work = new Work({
  op: tutorial.Operation.DIVIDE,
  num1: 1,
  num2: 0
});

Since we are leveraging TypeScript, we can leverage Interfaces as our constructor argument and avoid a lot of if statements in the constructor assignment. However, maybe this pushes the wrong assumptions or we don't want to push them. @nnance thoughts?

Rename interfaces with `I` prefix

I'm doing some more research into Interface creation for Structs (and other data types) to implement. These interfaces can also be used in the constructor as the typings for args, which fixes a problem I was having with nested container types.

I'm currently thinking the names should just be the Struct name with "Interface" appended. e.g. class MyStruct implements MyStructInterface @nnance thoughts?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.