diff --git a/.gitignore b/.gitignore index d7938db0..49d5b953 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ project/plugins/project/ # .arcconfig .arclint + +# npm +npm-debug.log diff --git a/README.md b/README.md index 35c933c9..45e121e1 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,6 @@ Courier is [Apache 2.0 Licensed](LICENSE.txt). For development and submitting pull requests, please see the [Contributing document](CONTRIBUTING.md). + + +Testing \ No newline at end of file diff --git a/flowtype/.gitignore b/flowtype/.gitignore new file mode 100644 index 00000000..79b9a21f --- /dev/null +++ b/flowtype/.gitignore @@ -0,0 +1,11 @@ +.gradle +build/ + +generator/build +testsuite/testsuiteTests/generated + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar diff --git a/flowtype/README.md b/flowtype/README.md new file mode 100644 index 00000000..d5eb71df --- /dev/null +++ b/flowtype/README.md @@ -0,0 +1,316 @@ +Typescript-Lite Courier Data Binding Generator +============================================== + +Experimental! + +Lightweight Courier bindings for Typescript. + +The guiding philosophy here was to achieve type safety with as small a runtime footprint as possible -- use the fact +that JSON is native to javascript to lightly define types and interfaces that describe your courier specs. + +These bindings will achieve their goal if: + + * You can just cast the result of a JSON HTTP response (or other JSON source) into the base expected + type and stay typesafe without having to cast anything more afterwards. + * You can hand-write JSON in your test-cases and API requests, and those test-cases + fail to compile when the record contract breaks. + * You can enjoy the sweet auto-complete and compile-time warnings you've come to enjoy from typescript. + +These bindings require Typescript 1.8+ + +Features missing in action +-------------------------- + +* **Coercer support**. The strong runtime component of Coercers don't gel easily with the philosophy of casting records + lightly into target typescript interfaces, so there wasn't an easy fit. + * Maybe these get included when Typescript-lite becomes just plain ol typescript. +* **Keyed maps**. Javascript objects naturally only support string-keyed maps. Since we are avoiding runtime + overhead as much as possible we did not want to introduce a new type, nor the means to serialize said type. + * Integrating `Immutable.js` would make a lot of sense when we want to expand into this direction. + * These bindings will coerce keyed maps into string-keyed maps (which they are on the wire anyways) +* **Non-JSON serialization**. Although courier supports more compact binary formats (PSON, Avro, BSON), Typescript-lite + bindings currently only supports valid JSON objects. +* **Default values**. As with the other points, default values require a runtime. +* **Flat-typed definitions**. This was just an oversight. Gotta add these. + +Running the generator from the command line +------------------------------------------- + +Build a fat jar from source and use that. See the below section *Building a fat jar*. + +How code is generated +--------------------- + +**Records:** + +* Records are generated as an interface within the typescript `namespace` specified by your record. + * If you don't use a namespace, the interface will be injected at the top-level + +e.g. the result of [Fortune.courier](https://github.com/coursera/courier/blob/master/reference-suite/src/main/courier/org/example/Fortune.courier): + +```typescript +// ./my-tslite-bindings/org.example.Fortune.ts +import { FortuneTelling } from "./org.example.FortuneTelling"; +import { DateTime } from "./org.example.common.DateTime"; + +/** + * A fortune. + */ +export interface Fortune { + + /** + * The fortune telling. + */ + telling : FortuneTelling; + + createdAt : DateTime; +} +``` + +**Enums:** + +* Enums are represented as [string literal types](https://basarat.gitbooks.io/typescript/content/docs/types/stringLiteralType.html). +* Convenience constants matching the string literals are provided despite having a runtime cost +* Unlike other bindings, Typescript-lite does not include an `UNKNOWN$` option. In case of wire inconsistency + you will have to just fall through to `undefined`. + +e.g. the result of [MagicEightBallAnswer.courier](https://github.com/coursera/courier/blob/master/reference-suite/src/main/courier/org/example/MagicEightBallAnswer.courier): + +```typescript +// ./my-tslite-bindings/org.example.MagicEightBallAnswer.ts +/** + * Magic eight ball answers. + */ +export type MagicEightBallAnswer = "IT_IS_CERTAIN" | "ASK_AGAIN_LATER" | "OUTLOOK_NOT_SO_GOOD" ; +export module MagicEightBallAnswer { + + export const IT_IS_CERTAIN: MagicEightBallAnswer = "IT_IS_CERTAIN"; + + export const ASK_AGAIN_LATER: MagicEightBallAnswer = "ASK_AGAIN_LATER"; + + export const OUTLOOK_NOT_SO_GOOD: MagicEightBallAnswer = "OUTLOOK_NOT_SO_GOOD"; + + export const all: Array = ["IT_IS_CERTAIN", "ASK_AGAIN_LATER", "OUTLOOK_NOT_SO_GOOD"]; +} +``` + +```typescript +// Some other file +// You can use it like this + +const answer: MagicEightBallAnswer = "IT_IS_CERTAIN"; +switch(answer) { + case MagicEightBallAnswer.IT_IS_CERTAIN: + // do something + break; + case MagicEightBallAnswer.ASK_AGAIN_LATER: + // do something + break; + default: + // you should probably always check this...in case you got some new unexpected + // value from a new version of the server software. This is the equivalent of + // testing UNKNOWN$ +} + +console.log(MagicEightBallAnswer.all); // logs all possible enum values to console +``` +**Arrays:** + +* Arrays are represented as typescript arrays. + +**Maps:** + +* Maps are represented as javascript objects, as interfaced by `courier.Map` +* Only string-keyed maps are currently supported. + +**Unions:** + +* Unions are represented as an intersection type between all the members of the union. +* A Run-time `unpack` accessor is provided to test each aspect of the union +* Unlike other serializers, no `UNKNOWN$` member is generated for the union. If all provided accessors end up yielding + undefined, then conclude that it was an unknown union member (see second example) + +e.g. The result of [FortuneTelling.pdsc](https://github.com/coursera/courier/blob/master/reference-suite/src/main/pegasus/org/example/FortuneTelling.pdsc): +```typescript +// file: my-tslite-bindings/org.example.FortuneTelling.ts +import { MagicEightBall } from "./org.example.MagicEightBall"; +import { FortuneCookie } from "./org.example.FortuneCookie"; + +export type FortuneTelling = FortuneTelling.FortuneCookieMember | FortuneTelling.MagicEightBallMember | FortuneTelling.StringMember; +export module FortuneTelling { + export interface FortuneTellingMember { + [key: string]: FortuneCookie | MagicEightBall | string; + } + + export interface FortuneCookieMember extends FortuneTellingMember { + "org.example.FortuneCookie": FortuneCookie; + } + + export interface MagicEightBallMember extends FortuneTellingMember { + "org.example.MagicEightBall": MagicEightBall; + } + + export interface StringMember extends FortuneTellingMember { + "string": string; + } + + export function unpack(union: FortuneTelling) { + return { + fortuneCookie: union["org.example.FortuneCookie"] as FortuneCookie, + magicEightBall: union["org.example.MagicEightBall"] as MagicEightBall, + string$: union["string"] as string + }; + } +} +``` + +Here's how you would use one: +```typescript +import { FortuneTelling } from "./my-tslite-bindings/org.example.FortuneTelling"; +import { MacigEightBall } from "./my-tslite-bindings/org.example.MagicEightBall"; +import { FortuneCookie } from "./my-tslite-bindings/org.example.FortuneCookie"; + +const telling: FortuneTelling = /* Get the union from somewhere...probably the wire */; + +const { fortuneCookie, magicEightBall, string_ } = FortuneTelling.unpack(telling); + +if (fortuneCookie) { + // do something with fortuneCookie +} else if (magicEightBall) { + // do something with magicEightBall +} else if (string$) { + // do something with str +} else { + throw 'a fit because no one will tell your fortune'; +} +``` + +Projections and Optionality +--------------------------- + +These bindings do not currently support projections. If you need to use projections, then +generate your bindings with Optionality of REQUIRED_FIELDS_MAY_BE_ABSENT rather than STRICT +as the 4th argument to the generator tool. + +That said, here is a good way we could evolve to support projections: + +I think [Intersection types](https://basarat.gitbooks.io/typescript/content/docs/types/type-system.html#intersection-type) may be a good approach in typescript + +Imagine the following courier type: + +``` +record Message { + id: string; + subject: string; + body: string; +} +``` + +If we wanted to support projections, we could generate the following types + +```typescript +module Message { + interface Id { + id: string; + } + interface Subject { + subject: string; + } + + interface Body { + body: string; + } +} +type Message = Message.Id & Message.Subject & Message.Body; +``` + +In your application code when you request some projection, instead of using the Message type you could just safely cast the message down to its component projections! For example: + +```typescript +function getMessageIdAndBody(id: string): Promise { + return http.get('/messages/' + id + '?projection=(id,body)').then((resp) => { + return resp.data as (Message.Id & Message.Body); + }) +} +``` + +Any attempt to access `message.subject` from the results of that function would of course fail at compile time. + + +Custom Types +------------ + +Custom Types allow any Typescript type to be bound to any pegasus primitive type. + +For example, say a schema has been defined to represent a "date time" as a unix timestamp long: + +``` +namespace org.example + +typeref DateTime = long +``` + +This results in a typescript file: +```typescript +// ./my-tslite-bindings/org.coursera.customtypes.DateTime.ts +export type DateTime = number; +``` + +JSON Serialization / Deserialization +------------------------------------ + +JSON serialization is trivial in typescript. Just take use `JSON.stringify` on any courier type that compiles. + +```typescript +import { Message } from "./my-tslite-bindings/org.coursera.records.Message"; + +const message: Message = {"body": "Hello Pegasus!"}; +const messageJson = JSON.stringify(message); +``` + +And of course you can read results as well. + +```typescript +import { Message } from "./my-tslite-bindings/org.coursera.records.Message"; + +const messageStr: string = /* Get the message string somehow */ +const message = JSON.parse(messageStr) as Message; +``` + +Runtime library +--------------- + +All generated Typescript-lite bindings depend on a `CourierRuntime.ts` class. This class provides the very minimal +functionality and type definitions necessary for generated bindings to work. + +Building from source +-------------------- + +See the main CONTRIBUTING document for details. + +Building a Fat Jar +------------------ + +```sh +$ sbt +> project typescript-lite-generator +> assembly +``` + +This will build a standalone "fat jar". This is particularly convenient for use as a standalone +commandline application. + +Testing +------- + +1. No testing has been done yet except verifying that the generated files from reference-suite pass `tsc`. + That's next on the list =) + +TODO +---- + +* [ ] Add support for flat type definitions +* [ ] Figure out the best way to distribute the 'fat jar'. +* [ ] Automate distribution of the Fat Jar +* [ ] Publish Fat Jar to remote repos? Typically fat jars should not be published to maven/ivy + repos, but maybe it should be hosted for easy download elsewhere? diff --git a/flowtype/generator-test/build.sbt b/flowtype/generator-test/build.sbt new file mode 100644 index 00000000..6cb856a3 --- /dev/null +++ b/flowtype/generator-test/build.sbt @@ -0,0 +1,45 @@ +import sbt.inc.Analysis + +name := "courier-flowtype-generator-test" + +packagedArtifacts := Map.empty // do not publish + +libraryDependencies ++= Seq( + ExternalDependencies.JodaTime.jodaTime) + +autoScalaLibrary := false + +crossPaths := false + +// Test Generator +forkedVmCourierGeneratorSettings + +forkedVmCourierMainClass := "org.coursera.courier.FlowtypeGenerator" + +forkedVmCourierClasspath := (dependencyClasspath in Runtime in flowtypeGenerator).value.files + +forkedVmSourceDirectory := (sourceDirectory in referenceSuite).value / "main" / "courier" + +forkedVmCourierDest := file("flowtype") / "testsuite" / "src" / "flowtype-bindings" + +forkedVmAdditionalArgs := Seq("STRICT") + +(compile in Compile) := { + (forkedVmCourierGenerator in Compile).value + + Analysis.Empty +} + +lazy val npmTest = taskKey[Unit]("Executes NPM test") + +npmTest in Test := { + (compile in Compile).value + + val result = """./flowtype/testsuite/full-build.sh"""! + + if (result != 0) { + throw new RuntimeException("NPM Build Failed") + } +} + +test in Test := (npmTest in Test).value diff --git a/flowtype/generator/.gitignore b/flowtype/generator/.gitignore new file mode 100644 index 00000000..19ec0db9 --- /dev/null +++ b/flowtype/generator/.gitignore @@ -0,0 +1,2 @@ +src/test/mainGeneratedPegasus +src/main/mainGeneratedPegasus diff --git a/flowtype/generator/build.sbt b/flowtype/generator/build.sbt new file mode 100644 index 00000000..6266157d --- /dev/null +++ b/flowtype/generator/build.sbt @@ -0,0 +1,17 @@ +import sbtassembly.AssemblyPlugin.defaultShellScript + +name := "courier-flowtype-generator" + +plainJavaProjectSettings + +libraryDependencies ++= Seq( + ExternalDependencies.Rythm.rythmEngine, + ExternalDependencies.Slf4j.slf4jSimple) + +// Fat Jar +mainClass in assembly := Some("org.coursera.courier.FlowtypeGenerator") + +assemblyOption in assembly := (assemblyOption in assembly).value.copy(prependShellScript = Some(defaultShellScript)) + +assemblyJarName in assembly := s"${name.value}-${version.value}.jar" + diff --git a/flowtype/generator/src/main/java/org/coursera/courier/FlowtypeGenerator.java b/flowtype/generator/src/main/java/org/coursera/courier/FlowtypeGenerator.java new file mode 100644 index 00000000..965454b7 --- /dev/null +++ b/flowtype/generator/src/main/java/org/coursera/courier/FlowtypeGenerator.java @@ -0,0 +1,171 @@ +/* + * Copyright 2017 Coursera Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.coursera.courier; + +import com.linkedin.data.schema.DataSchema; +import com.linkedin.pegasus.generator.GeneratorResult; +import com.linkedin.pegasus.generator.spec.*; +import org.apache.commons.io.IOUtils; +import org.coursera.courier.api.DefaultGeneratorRunner; +import org.coursera.courier.api.GeneratedCode; +import org.coursera.courier.api.GeneratedCodeTargetFile; +import org.coursera.courier.api.GeneratorRunnerOptions; +import org.coursera.courier.api.PegasusCodeGenerator; +import org.coursera.courier.lang.DocCommentStyle; +import org.coursera.courier.lang.PoorMansCStyleSourceFormatter; +import org.coursera.courier.flowtype.GlobalConfig; +import org.coursera.courier.flowtype.FlowtypeProperties; +import org.coursera.courier.flowtype.FlowtypeSyntax; +import org.rythmengine.RythmEngine; +import org.rythmengine.exception.RythmException; +import org.rythmengine.resource.ClasspathResourceLoader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; + +/** + * Courier code generator for Flowtype. + */ +public class FlowtypeGenerator implements PegasusCodeGenerator { + private static final FlowtypeProperties.Optionality defaultOptionality = + FlowtypeProperties.Optionality.REQUIRED_FIELDS_MAY_BE_ABSENT; + + private static final String flowCodeHeader = + "// @flow\n// WARNING: This is a generated file, do NOT modify.\n"; + + private final GlobalConfig globalConfig; + private final RythmEngine engine; + + public static void main(String[] args) throws Throwable { + if (args.length < 3 || args.length > 5) { + throw new IllegalArgumentException( + "Usage: targetPath resolverPath1[:resolverPath2]+ sourcePath1[:sourcePath2]+ [REQUIRED_FIELDS_MAY_BE_ABSENT|STRICT] [EQUATABLE]"); + } + String targetPath = args[0]; + String resolverPath = args[1]; + String sourcePathString = args[2]; + String[] sourcePaths = sourcePathString.split(":"); + + FlowtypeProperties.Optionality optionality = defaultOptionality; + if (args.length > 3) { + optionality = FlowtypeProperties.Optionality.valueOf(args[3]); + } + + boolean equatable = false; + if (args.length > 4) { + if (!args[4].equals("EQUATABLE")) { + throw new IllegalArgumentException("If present 4th argument must be 'EQUATABLE'"); + } + equatable = true; + } + + GeneratorRunnerOptions options = + new GeneratorRunnerOptions(targetPath, sourcePaths, resolverPath); + + GlobalConfig globalConfig = new GlobalConfig(optionality, equatable, false); + GeneratorResult result = + new DefaultGeneratorRunner().run(new FlowtypeGenerator(globalConfig), options); + + for (File file: result.getTargetFiles()) { + System.out.println(file.getAbsolutePath()); + } + + InputStream runtime = FlowtypeGenerator.class.getClassLoader().getResourceAsStream("runtime/CourierRuntime.js"); + IOUtils.copy(runtime, new FileOutputStream(new File(targetPath, "CourierRuntime.js"))); + } + + public FlowtypeGenerator() { + this(new GlobalConfig( + defaultOptionality, + false, + false)); + } + + public FlowtypeGenerator(GlobalConfig globalConfig) { + this.globalConfig = globalConfig; + this.engine = new RythmEngine(); + this.engine.registerResourceLoader(new ClasspathResourceLoader(engine, "/")); + } + + public static class FlowCompilationUnit extends GeneratedCodeTargetFile { + public FlowCompilationUnit(String name, String namespace){ + super(name, namespace, "js"); + } + } + + private static final PoorMansCStyleSourceFormatter formatter = + new PoorMansCStyleSourceFormatter(2, DocCommentStyle.ASTRISK_MARGIN); + + /** + * See {@link org.coursera.courier.tslite.FlowtypeProperties} for customization options. + */ + @Override + public GeneratedCode generate(ClassTemplateSpec templateSpec) { + String code = this.flowCodeHeader; + FlowtypeProperties FlowtypeProperties = globalConfig.lookupFlowtypeProperties(templateSpec); + if (FlowtypeProperties.omit) return null; + + FlowtypeSyntax syntax = new FlowtypeSyntax(FlowtypeProperties); + try { + if (templateSpec instanceof RecordTemplateSpec) { + code += engine.render("rythm/record.txt", syntax.new FlowtypeRecordSyntax((RecordTemplateSpec) templateSpec)); + } else if (templateSpec instanceof EnumTemplateSpec) { + code += engine.render("rythm/enum.txt", syntax.new FlowtypeEnumSyntax((EnumTemplateSpec) templateSpec)); + } else if (templateSpec instanceof UnionTemplateSpec) { + code += engine.render("rythm/union.txt", syntax.new FlowtypeUnionSyntax((UnionTemplateSpec) templateSpec)); + } else if (templateSpec instanceof TyperefTemplateSpec) { + TyperefTemplateSpec typerefSpec = (TyperefTemplateSpec) templateSpec; + code += engine.render("rythm/typeref.txt", syntax.FlowtypeTyperefSyntaxCreate(typerefSpec)); + } else if (templateSpec instanceof FixedTemplateSpec) { + code += engine.render("rythm/fixed.txt", syntax.TSFixedSyntaxCreate((FixedTemplateSpec) templateSpec)); + } else { + return null; // Indicates that we are declining to generate code for the type (e.g. map or array) + } + } catch (RythmException e) { + throw new RuntimeException( + "Internal error in generator while processing " + templateSpec.getFullName(), e); + } + FlowCompilationUnit compilationUnit = + new FlowCompilationUnit( + templateSpec.getFullName(), ""); + code = formatter.format(code); + return new GeneratedCode(compilationUnit, code); + } + + @Override + public Collection generatePredef() { + return Collections.emptySet(); + } + + @Override + public Collection definedSchemas() { + return Collections.emptySet(); + } + + @Override + public String buildLanguage() { + return "typescript"; + } + + @Override + public String customTypeLanguage() { + return "typescript"; + } +} diff --git a/flowtype/generator/src/main/java/org/coursera/courier/flowtype/FlowtypeProperties.java b/flowtype/generator/src/main/java/org/coursera/courier/flowtype/FlowtypeProperties.java new file mode 100644 index 00000000..d7c539d1 --- /dev/null +++ b/flowtype/generator/src/main/java/org/coursera/courier/flowtype/FlowtypeProperties.java @@ -0,0 +1,67 @@ +package org.coursera.courier.flowtype; + +/** + * Customizable properties that may be added to a Pegasus schema. + * + * Example usage: + * + * + * { + * "name": "Fortune", + * "namespace": "org.example", + * "type": "record", + * "fields": [ ... ], + * "typescript": { + * "optionality": "STRICT" + * } + * } + * + */ +public class FlowtypeProperties { + + /** + * "optionality" property. + * + * Typescript representations of Pegasus primitive types supported by this generator. + */ + public enum Optionality { + + /** + * Allows required fields to be absent, useful when working with projections. + * + * "undefined" is used to represent un-projected fields (required or optional) as well as absent + * optional fields. + */ + REQUIRED_FIELDS_MAY_BE_ABSENT, + + // TODO(jbetz): Remove as soon as we've migrated away from this usage pattern. + /** + * WARNING: this mode is unsafe when used in conjunction with projections, as a read/modify/coercerOutput + * pattern on a projection could result in the default value of primitives (e.g. 0 for ints) + * to be accidentally written. + * + * Required fields generated as non-optional Typescript properties. + * + * Optional fields are generated as optional Typescript properties, where an absent optional field + * value is represented as `nil`. + * + * When reading JSON, if a required field is absent, the field in data binding + * will default to the schema defined default value. + * + * When writing JSON, all required primitive fields will be written, even if they are the + * schema defined default value. + */ + STRICT + } + + public final Optionality optionality; + public final boolean equatable; + public final boolean omit; + + public FlowtypeProperties(Optionality optionality, boolean equatable, boolean omit) { + this.optionality = optionality; + this.equatable = equatable; + this.omit = omit; + } + +} diff --git a/flowtype/generator/src/main/java/org/coursera/courier/flowtype/FlowtypeSyntax.java b/flowtype/generator/src/main/java/org/coursera/courier/flowtype/FlowtypeSyntax.java new file mode 100644 index 00000000..71786c9b --- /dev/null +++ b/flowtype/generator/src/main/java/org/coursera/courier/flowtype/FlowtypeSyntax.java @@ -0,0 +1,1050 @@ +/* + * Copyright 2016 Coursera Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.coursera.courier.flowtype; + +import com.linkedin.data.DataMap; +import com.linkedin.data.schema.DataSchema; +import com.linkedin.data.schema.DataSchema.Type; +import com.linkedin.data.schema.EnumDataSchema; +import com.linkedin.data.schema.NamedDataSchema; +import com.linkedin.data.schema.PrimitiveDataSchema; +import com.linkedin.data.schema.RecordDataSchema; +import com.linkedin.data.schema.TyperefDataSchema; +import com.linkedin.data.schema.UnionDataSchema; +import com.linkedin.pegasus.generator.spec.*; +import org.coursera.courier.api.ClassTemplateSpecs; +import org.coursera.courier.lang.DocCommentStyle; +import org.coursera.courier.lang.DocEscaping; +import org.coursera.courier.flowtype.FlowtypeProperties.Optionality; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Map; + +/** + * Main work-horse for populating the ts-lite Rythm templates. + * + * Most work delegates to inner classes, so you probably want to look them (linked below) + * + * Specifically, {@link FlowtypeEnumSyntax}, {@link FlowtypeUnionSyntax}, {@link FlowtypeRecordSyntax}, and {@link FlowtypeTyperefSyntax} are + * used directly to populate the templates. + * + * @see TSPrimitiveTypeSyntax + * @see FlowtypeEnumSyntax + * @see TSArraySyntax + * @see FlowtypeMapSyntax + * @see FlowtypeTyperefSyntax + * @see FlowtypeRecordSyntax + * @see TSFixedSyntax + * @see FlowtypeRecordSyntax + * @see FlowtypeUnionSyntax + */ +public class FlowtypeSyntax { + + /** Config properties passed from the command line parser */ + private final FlowtypeProperties FlowtypeProperties; + + public FlowtypeSyntax(FlowtypeProperties FlowtypeProperties) { + this.FlowtypeProperties = FlowtypeProperties; + } + + /** + * Varying levels of reserved keywords copied from https://github.com/Microsoft/TypeScript/issues/2536 + **/ + private static final Set tsKeywords = new HashSet(Arrays.asList(new String[]{ + // Reserved Words + "break", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "enum", + "export", + "extends", + "false", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "new", + "null", + "return", + "super", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", + + // Strict Mode Reserved Words + "as", + "implements", + "interface", + "let", + "package", + "private", + "protected", + "public", + "static", + "yield", + + // Contextual Keywords + "any", + "boolean", + "constructor", + "declare", + "get", + "module", + "require", + "number", + "set", + "string", + "symbol", + "type", + "from", + "of" + })); + + + /** Different choices for how to escaping symbols that match reserved ts keywords. */ + private static enum EscapeStrategy { + /** Adds a dollar sign after the symbol name when escaping. e.g.: class becomes class$ */ + MANGLE, + + /** Quotes the symbol when escaping. e.g.: class becomes "class" */ + QUOTE + } + + /** + * Returns the escaped Pegasus symbol for use in Typescript source code. + * + * Pegasus symbols must be of the form [A-Za-z_], so this routine simply checks if the + * symbol collides with a typescript keyword, and if so, escapes it. + * + * @param symbol the symbol to escape + * @param strategy which strategy to use in escaping + * + * @return the escaped Pegasus symbol. + */ + private static String escapeKeyword(String symbol, EscapeStrategy strategy) { + if (tsKeywords.contains(symbol)) { + if (strategy.equals(EscapeStrategy.MANGLE)) { + return symbol + "$"; + } else { + return "\"" + symbol + "\""; + } + } else { + return symbol; + } + } + + /** + * Creates a valid typescript import string given a type name (e.g. "Fortune") and the module name, + * which is usually the pegasus object's namespace. + * + * @param typeName Name of the type to import (e.g. "Fortune") + * @param moduleName That same type's namespace (e.g. "org.example") + * + * @return A fully formed import statement. e.g: import { Fortune } from "./org.example.Fortune" + **/ + private static String importString(String typeName, String moduleName) { + return new StringBuilder() + .append("import type { ") + .append(typeName) + .append(" } from \"./") + .append(moduleName) + .append(".") + .append(typeName) + .append("\";") + .toString(); + } + + /** + * Return a full tsdoc for a type. + * + * @param doc the doc string in the type's DataSchema. + * @param deprecation the object listed under the schema's "deprecation" property + * + * @return a fully formed tsdoc for the type. + */ + private static String docComment(String doc, Object deprecation /* nullable */) { + StringBuilder docStr = new StringBuilder(); + + if (doc != null) { + docStr.append(doc.trim()); + } + + if (deprecation != null) { + docStr.append("\n\n").append("@deprecated"); + if (deprecation instanceof String) { + docStr.append(" ").append(((String)deprecation).trim()); + } + } + return DocEscaping.stringToDocComment(docStr.toString(), DocCommentStyle.ASTRISK_MARGIN); + } + + + /** + * Takes a set of imports constructed with {@link #importString}, and produces a valid import block + * for use at the top of a typescript source file + * + * @param imports the set of imports, each of which is a valid import line in typescript + * @return the import block, on separate lines. + */ + private static String flattenImports(Set imports) { + StringBuilder sb = new StringBuilder(); + + for (String import_: imports) { + sb.append(import_).append("\n"); + } + + return sb.toString(); + } + + /** Describes any type we are representing in the generated typescript */ + interface FlowtypeTypeSyntax { + + /** Return the simple name of the type, in valid typescript. "number" or "string" for example. */ + public String typeName(); + + /** + * Return the set of modules that must be imported in order for some other module + * to use this type. + **/ + public Set modulesRequiredToUse(); + } + + /** + * Describes any type that can be enclosed by another. According to the restli spec this only applies + * to anonymous unions. https://github.com/linkedin/rest.li/wiki/DATA-Data-Schema-and-Templates + **/ + private interface TSEnclosedTypeSyntax { + public String typeNameQualifiedByEnclosedType(); + } + + /** + * Create a TS*Syntax class around the provided ClassTemplate. + * + * That class will perform the heavy lifting of rendering TS-specific strings into the template. + * + * @param template the ClassTemplate + * @return a TS*Syntax class (see {@link FlowtypeSyntax} class-level docs for more info) + */ + private FlowtypeTypeSyntax createTypeSyntax(ClassTemplateSpec template) { + if (template instanceof RecordTemplateSpec) { + return new FlowtypeRecordSyntax((RecordTemplateSpec) template); + } else if (template instanceof TyperefTemplateSpec) { + return FlowtypeTyperefSyntaxCreate((TyperefTemplateSpec) template); + } else if (template instanceof FixedTemplateSpec) { + return TSFixedSyntaxCreate((FixedTemplateSpec) template); + } else if (template instanceof EnumTemplateSpec) { + return new FlowtypeEnumSyntax((EnumTemplateSpec) template); + } else if (template instanceof PrimitiveTemplateSpec) { + return new TSPrimitiveTypeSyntax((PrimitiveTemplateSpec) template); + } else if (template instanceof MapTemplateSpec) { + return new FlowtypeMapSyntax((MapTemplateSpec) template); + } else if (template instanceof ArrayTemplateSpec) { + return new TSArraySyntax((ArrayTemplateSpec) template); + } else if (template instanceof UnionTemplateSpec) { + return new FlowtypeUnionSyntax((UnionTemplateSpec) template); + } else { + throw new RuntimeException("Unrecognized template spec: " + template + " with schema " + template.getSchema()); + } + } + + /** Convenience wrapper around {@link #createTypeSyntax(ClassTemplateSpec)}. */ + private FlowtypeTypeSyntax createTypeSyntax(DataSchema schema) { + return createTypeSyntax(ClassTemplateSpec.createFromDataSchema(schema)); + } + + /** + * Returns the type name, prefaced with the enclosing class name if there was one. + * + * For example, a standalone union called MyUnion will just return "MyUnion". + * If that same union were enclosed within MyRecord, this would return "MyRecord.MyUnion". + **/ + String typeNameQualifiedByEnclosingClass(FlowtypeTypeSyntax syntax) { + // if (syntax instanceof TSEnclosedTypeSyntax) { + // return ((TSEnclosedTypeSyntax) syntax).typeNameQualifiedByEnclosedType(); + // } else { + // return syntax.typeName(); + // } + return syntax.typeName(); + } + + /** TS-specific syntax for Maps */ + private class FlowtypeMapSyntax implements FlowtypeTypeSyntax { + private final MapTemplateSpec _template; + + FlowtypeMapSyntax(MapTemplateSpec _template) { + this._template = _template; + } + + @Override + public String typeName() { + // (This comment is duplicated from TSArraySyntax.typeName for your benefit) + // Sadly the behavior of this function is indirectly controlled by the one calling it: FlowRecordFieldSyntax. + // That class has the unfortunate behavior that it can produce 2 different ClassTemplateSpecs, one of which works for + // some cases, and one of which works for the others. See its own "typeName" definition for details but essentially + // it will give us one of the ClassTemplateSpecs and call typeName. If we then return null + // then it will give it a shot with the other ClassTemplateSpec. Unfortunately we have to do this because if + // we try to just use the first one, we will return "Map". This is also why we special-case unions here. + // we have to access a specific ClassTemplate + boolean valueIsUnion = _template.getValueClass() instanceof UnionTemplateSpec; + FlowtypeTypeSyntax itemTypeSyntax = valueIsUnion? createTypeSyntax(_template.getValueClass()): _valueTypeSyntax(); + String valueTypeName = typeNameQualifiedByEnclosingClass(itemTypeSyntax); + return valueTypeName == null? null: "Map<" + valueTypeName + ">"; + } + + @Override + public Set modulesRequiredToUse() { + Set modules = new HashSet<>(); + modules.add("import type { Map } from \"./CourierRuntime\";"); // Our runtime contains a typedef for Map + modules.addAll(_valueTypeSyntax().modulesRequiredToUse()); // Need the map's value type to compile code that uses this type. + return modules; + } + + // + // Private FlowtypeMapSyntax members + // + private FlowtypeTypeSyntax _valueTypeSyntax() { + return createTypeSyntax(_template.getSchema().getValues()); + } + } + + /** TS-specific syntax for Arrays */ + private class TSArraySyntax implements FlowtypeTypeSyntax { + private final ArrayTemplateSpec _template; + + TSArraySyntax(ArrayTemplateSpec _template) { + this._template = _template; + } + + @Override + public String typeName() { + // Sadly the behavior of this function is indirectly controlled by the one calling it: FlowRecordFieldSyntax. + // That class has the unfortunate behavior that it can produce 2 different ClassTemplateSpecs, one of which works for + // some cases, and one of which works for the others. See its own "typeName" definition for details but essentially + // it will give us one of the ClassTemplateSpecs and call typeName. If we then return null + // then it will give it a shot with the other ClassTemplateSpec. Unfortunately we have to do this because if + // we try to just use the first one, we will return "Array". This is also why we special-case unions here. + // we have to access a specific ClassTemplate + boolean itemIsUnion = _template.getItemClass() instanceof UnionTemplateSpec; + FlowtypeTypeSyntax itemTypeSyntax = itemIsUnion? createTypeSyntax(_template.getItemClass()): _itemTypeSyntax(); + String itemTypeName = typeNameQualifiedByEnclosingClass(itemTypeSyntax); + return itemTypeName == null? null: "Array<" + itemTypeName + ">"; + } + + @Override + public Set modulesRequiredToUse() { + return _itemTypeSyntax().modulesRequiredToUse(); // Need to import the array's index type to compile code that uses this type + } + + // + // Private TSArraySyntax members + // + private FlowtypeTypeSyntax _itemTypeSyntax() { + return createTypeSyntax(_template.getSchema().getItems()); + } + } + + /** Pegasus types that should be rendered as "number" in typescript */ + private static final Set TS_NUMBER_TYPES = new HashSet<>( + Arrays.asList( + new Type[] { Type.INT, Type.LONG, Type.FLOAT, Type.DOUBLE } + ) + ); + + /** Pegasus types that should be rendered as "string" in typescript */ + private static final Set TS_STRING_TYPES = new HashSet<>( + Arrays.asList( + new Type[] { Type.STRING, Type.BYTES, Type.FIXED } + ) + ); + + /** TS-specific syntax for all primitive types: Integer, Long, Float, Double, Boolean, String, Byte. */ + private class TSPrimitiveTypeSyntax implements FlowtypeTypeSyntax { + private final PrimitiveTemplateSpec _template; + private final PrimitiveDataSchema _schema; + + TSPrimitiveTypeSyntax(PrimitiveTemplateSpec _template) { + this._template = _template; + this._schema = _template.getSchema(); + } + + @Override + public String typeName() { + Type schemaType = _schema.getType(); + if (TS_NUMBER_TYPES.contains(schemaType)) { + return "number"; + } else if (TS_STRING_TYPES.contains(schemaType)) { + return "string"; + } else if (schemaType == Type.BOOLEAN) { + return "boolean"; + } else { + throw new IllegalArgumentException("Unexpected type " + schemaType + " in schema " + _schema); + } + } + + @Override + public Set modulesRequiredToUse() { + return new HashSet<>(); // using a primitive requires no imports + } + } + + /** + * Helper class that more-or-less wraps {@link NamedDataSchema}. + * + * Helps reduce code bloat for Records, Enums, and Typerefs. + **/ + private class FlowtypeNamedTypeSyntax { + private final NamedDataSchema _dataSchema; + + public FlowtypeNamedTypeSyntax(NamedDataSchema _dataSchema) { + this._dataSchema = _dataSchema; + } + + public String typeName() { + return FlowtypeSyntax.escapeKeyword(this._dataSchema.getName(), EscapeStrategy.MANGLE); + } + + public String docString() { + return docComment( + _dataSchema.getDoc(), + _dataSchema.getProperties().get("deprecated") + ); + } + + public Set modulesRequiredToUse() { + Set modules = new HashSet<>(); + // Named types get their own files, so you have to import them in order to use them. + modules.add(importString(_dataSchema.getName(), _dataSchema.getNamespace())); + return modules; + } + } + + /** TS syntax for Fixed types. */ + public class TSFixedSyntax implements FlowtypeTypeSyntax { + private final FixedTemplateSpec _template; + private final FlowtypeNamedTypeSyntax _namedSyntax; + + public TSFixedSyntax(FixedTemplateSpec template, FlowtypeNamedTypeSyntax namedSyntax) { + this._template = template; + this._namedSyntax = namedSyntax; + } + + public String docString() { + return _namedSyntax.docString(); + } + + public String typeName() { + return _namedSyntax.typeName(); + } + + @Override + public Set modulesRequiredToUse() { + return _namedSyntax.modulesRequiredToUse(); + } + } + + /** Create a new TSFixedSyntax */ + public TSFixedSyntax TSFixedSyntaxCreate(FixedTemplateSpec template) { + return new TSFixedSyntax(template, new FlowtypeNamedTypeSyntax(template.getSchema())); + } + + /** + * Flow representation of a Union type's member (e.g. the "int" in "union[int]"). + */ + public class FlowtypeUnionMemberSyntax { + private final FlowtypeUnionSyntax _parentSyntax; + private final UnionDataSchema _schema; + private final UnionTemplateSpec.Member _member; + + public FlowtypeUnionMemberSyntax(FlowtypeUnionSyntax _parentSyntax, UnionDataSchema _schema, UnionTemplateSpec.Member _member) { + this._parentSyntax = _parentSyntax; + this._schema = _schema; + this._member = _member; + } + + /** + * Provides a partially-qualified representation of this type's "Member" sister. + * For example, if you had a courier union[int] typeref as "MyUnion", this method would + * return "MyUnion.IntMember". + **/ + String fullUnionMemberTypeName() { + return _parentSyntax.typeName() + "." + this.unionMemberTypeName(); + } + + /** + * Returns the symbol used to access this union member's index in the union's "unpack" return object. + * + * For example, given union[FortuneCookie], the return object from "unpack" would be { fortuneCookie: union["namespace.FortuneCookie"] as FortuneCookie } + */ + public String unpackString() { + DataSchema schema = _memberSchema(); + String unpackNameBase; + if (schema instanceof PrimitiveDataSchema) { + unpackNameBase = schema.getUnionMemberKey(); + } else { + unpackNameBase = _memberTypeSyntax().typeName(); + } + + String punctuationEscaped = unpackNameBase.replaceAll("[\\p{Punct}\\p{Space}]", ""); + String lowerCased = Character.toLowerCase(punctuationEscaped.charAt(0)) + punctuationEscaped.substring(1); + + return escapeKeyword(lowerCased, EscapeStrategy.MANGLE); + } + + /** + * Returns the union member class name for the given {@link ClassTemplateSpec} as a Typescript + * source code string. + * + * @return a typescript source code string identifying the union member. + */ + public String unionMemberTypeName() { + DataSchema memberSchema = _memberSchema(); + Type memberType = _memberSchema().getType(); + if (memberSchema.isPrimitive() || memberType == Type.MAP || memberType == Type.ARRAY) { + String unionMemberKey = _memberSchema().getUnionMemberKey(); + String camelCasedName = Character.toUpperCase(unionMemberKey.charAt(0)) + unionMemberKey.substring(1); + return camelCasedName + "Member"; // IntMember, DoubleMember, FixedMember etc + } else if (memberSchema instanceof NamedDataSchema) { + String className = ((NamedDataSchema) memberSchema).getName(); + return className + "Member"; // e.g: FortuneCookieMember + } else { + throw new IllegalArgumentException("Don't know how to handle schema of type " + memberSchema.getType()); + } + } + + public String unionMemberKey() { + return _member.getSchema().getUnionMemberKey(); + } + + public String typeName() { + return _memberTypeSyntax().typeName(); + } + + /** The set of modules imports that need to be included in order to use the type represented by this union member */ + Set typeModules() { + return _memberTypeSyntax().modulesRequiredToUse(); + } + + // + // Private UnionMemberSyntax members + // + private DataSchema _memberSchema() { + return _member.getSchema(); + } + private FlowtypeTypeSyntax _memberTypeSyntax() { + return createTypeSyntax(_member.getSchema()); + } + } + + /** TS-specific representation of a Union type. */ + public class FlowtypeUnionSyntax implements FlowtypeTypeSyntax, TSEnclosedTypeSyntax { + private final UnionTemplateSpec _template; + private final UnionDataSchema _schema; + + public FlowtypeUnionSyntax(UnionTemplateSpec _template) { + this._template = _template; + this._schema = _template.getSchema(); + } + + @Override + public String typeNameQualifiedByEnclosedType() { + if (_template.getEnclosingClass() != null) { + return createTypeSyntax(_template.getEnclosingClass()).typeName() + "." + this.typeName(); + } else { + return this.typeName(); + } + } + + @Override + public String typeName() { + if (_template.getTyperefClass() != null) { + // If this union was typerefed then just use the typeref name + FlowtypeTyperefSyntax refSyntax = FlowtypeTyperefSyntaxCreate(_template.getTyperefClass()); + return refSyntax.typeName(); + } else { + // I actually never figured out why this works, so I'm very sorry if you're dealing + // with the repercussions here. + return escapeKeyword(this._template.getClassName(), EscapeStrategy.MANGLE); + } + } + + /** Return the whole typescript import block for the file in which this union is declared. */ + public String imports() { + Set allImports = new HashSet<>(); + + // Only print out the imports for non-enclosed union types. Enclosed ones will be handled + // by the enclosing record. + if (!_isEnclosedType()) { + for (FlowtypeUnionMemberSyntax member: this.members()) { + allImports.addAll(member.typeModules()); + } + } + + return flattenImports(allImports); + } + + @Override + public Set modulesRequiredToUse() { + Set modules = new HashSet(); + // enclosed types dont report modules -- their enclosing types will do so for them! + if (!_isEnclosedType() && this.typeName() != null) { + modules.add(importString(this.typeName(), this._template.getNamespace())); + } + return modules; + } + + public String docString() { + if (this._template.getTyperefClass() != null) { + return new FlowtypeNamedTypeSyntax(this._template.getTyperefClass().getSchema()).docString(); + } else { + return ""; + } + } + + /** + * Produces the "MyUnionMember" typename. + * + * For example, union[int, string] produces a few extra types: IntMember, StringMember, etc. Each of those inherit + * from "MyUnionMember" (or whatever your union type is called) + **/ + public String memberBaseTypeName() { + return this.typeName() + "Member"; + } + + /** + * Given union[int, string, FortuneCookie] this returns the typescript equivalent: "number" | "string" | FortuneCookie + **/ + public String unionTypeExpression() { + StringBuilder sb = new StringBuilder(); + + List members = this.members(); + for (int i = 0; i < members.size(); i++) { + boolean isLast = (i == members.size() - 1); + FlowtypeUnionMemberSyntax member = members.get(i); + sb.append(member.typeName()); + + if (!isLast) { + sb.append(" | "); + } + } + + return sb.toString(); + } + + /** + * The same as {@link #unionTypeExpression}, but for the *Member interfaces that provide string-lookup. + * + * So given union[int, string, FortuneCookie] this returns "MyUnion.IntMember | MyUnion.StringMember | MyUnion.FortuneCookieMember" + * + */ + public String memberUnionTypeExpression() { + List members = this.members(); + + if (members.isEmpty()) { + return "void"; + } else { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < members.size(); i++) { + boolean isLast = (i == members.size() - 1); + FlowtypeUnionMemberSyntax member = members.get(i); + // sb.append(member.fullUnionMemberTypeName()); + sb.append(member.typeName()); + + if (!isLast) { + sb.append(" | "); + } + } + + return sb.toString(); + } + } + + /** Return the syntax for each member */ + public List members() { + List memberSyntax = new ArrayList<>(); + + for (UnionTemplateSpec.Member member : this._template.getMembers()) { + memberSyntax.add(new FlowtypeUnionMemberSyntax(this, _schema, member)); + } + + return memberSyntax; + } + + /** Returns true in the usual case that this isn't some stupid empty union. */ + public boolean requiresCompanionModule() { + return !this._template.getMembers().isEmpty(); + } + + private boolean _isEnclosedType() { + return _template.getEnclosingClass() != null; + } + } + + /** The Flow representation of a single field in a Record */ + public class FlowRecordFieldSyntax implements FlowtypeTypeSyntax { + private final RecordTemplateSpec _template; + private final RecordDataSchema _schema; + private final RecordTemplateSpec.Field _field; + + public FlowRecordFieldSyntax(RecordTemplateSpec _template, RecordTemplateSpec.Field _field) { + this._template = _template; + this._schema = _template.getSchema(); + this._field = _field; + } + + @Override + public Set modulesRequiredToUse() { + Set modules = new HashSet<>(); + // since this record lives in its own file you have to import it to use it. + modules.add(importString(_schema.getNamespace(), this.typeName())); + return modules; + } + + /** The typescript property for getting this field. */ + public String accessorName() { + return escapeKeyword(_schemaField().getName(), EscapeStrategy.QUOTE); + } + + public String typeName() { + // To resolve type name we have to determine whether to use the DataSchema in _field.getType() or + // the one in _field.getSchemaField().getType(). We reach first for the schemaField as it does not swallow + // Typerefs. (e.g. if a type was defined as CustomInt, it will give us the string CustomInt, whereas + // field.getType() would dereference all the way to the bottom). + // + // The only problem with schemaField is that it _does_ swallow the type names for enclosed unions. ARGH + // can we catch a break?? Thankfully in the case of the enclosed union it ends up returning null, so + // we back off to _field.getType() if schemaField returned null. + FlowtypeTypeSyntax candidateSyntax = createTypeSyntax(_schemaField().getType()); + if (candidateSyntax.typeName() == null || "".equals(candidateSyntax)) { + candidateSyntax = createTypeSyntax(_field.getType()); + } + + return typeNameQualifiedByEnclosingClass(candidateSyntax); + } + + public String docString() { + return docComment( + _schemaField().getDoc(), + _schemaField().getProperties().get("deprecated") + ); + } + + /** The modules that the containing Record module has to import in order to compile. */ + public Set typeModules() { + return _fieldTypeSyntax().modulesRequiredToUse(); + } + + /** + * Just returns a "?" if this was an optional field either due to being decalred optional, or opting not to pass + * the STRICT directive into the generator. + **/ + public String questionMarkIfOptional() { + boolean isFieldOptional = _schemaField().getOptional(); + boolean markFieldAsOptional = isFieldOptional || FlowtypeProperties.optionality == Optionality.REQUIRED_FIELDS_MAY_BE_ABSENT; + + return markFieldAsOptional? "?": ""; + } + + // + // Private members + // + private RecordDataSchema.Field _schemaField() { + return _field.getSchemaField(); + } + private FlowtypeTypeSyntax _fieldTypeSyntax() { + return createTypeSyntax(_schemaField().getType()); + } + } + + /** Flow-specific syntax for Records */ + public class FlowtypeRecordSyntax implements FlowtypeTypeSyntax { + private final RecordTemplateSpec _template; + private final RecordDataSchema _schema; + private final FlowtypeNamedTypeSyntax _namedTypeSyntax; + + public FlowtypeRecordSyntax(RecordTemplateSpec _template) { + this._template = _template; + this._schema = _template.getSchema(); + this._namedTypeSyntax = new FlowtypeNamedTypeSyntax(_schema); + } + + public String docString() { + return _namedTypeSyntax.docString(); + } + + public List fields() { + List fields = new ArrayList<>(); + + for (RecordTemplateSpec.Field fieldSpec: _template.getFields()) { + fields.add(new FlowRecordFieldSyntax(_template, fieldSpec)); + } + + return fields; + } + + public Set enclosedUnions() { + Set unions = new HashSet<>(); + for (ClassTemplateSpec spec: ClassTemplateSpecs.allContainedTypes(_template)) { + if (spec instanceof UnionTemplateSpec) { + unions.add(new FlowtypeUnionSyntax((UnionTemplateSpec) spec)); + } + } + + return unions; + } + + @Override + public Set modulesRequiredToUse() { + return _namedTypeSyntax.modulesRequiredToUse(); + } + + public String typeName() { + return escapeKeyword(_schema.getName(), EscapeStrategy.MANGLE); + } + + /** + * Returns true if a companion module needs to be declared for this record's interface. This is true if the record + * has enclosing types that must be defined within the record's namespace. + **/ + public boolean requiresCompanionModule() { + return !ClassTemplateSpecs.allContainedTypes(_template).isEmpty(); + } + + /** The complete flowtype import block for this record */ + public String imports() { + Set imports = new HashSet<>(); + + for (FlowRecordFieldSyntax fieldSyntax: this.fields()) { + if (fieldSyntax.typeName() != this.typeName()) { + imports.addAll(fieldSyntax.typeModules()); + } + } + + for (FlowtypeUnionSyntax union: this.enclosedUnions()) { + for (FlowtypeUnionMemberSyntax unionMember: union.members()) { + imports.addAll(unionMember.typeModules()); + } + } + + return flattenImports(imports); + } + } + + /** Flowtype syntax for typerefs. */ + public class FlowtypeTyperefSyntax implements FlowtypeTypeSyntax { + private final TyperefTemplateSpec _template; + private final TyperefDataSchema _dataSchema; + private final FlowtypeNamedTypeSyntax _namedTypeSyntax; + + public FlowtypeTyperefSyntax(TyperefTemplateSpec _template, TyperefDataSchema _dataSchema, FlowtypeNamedTypeSyntax _namedTypeSyntax) { + this._template = _template; + this._dataSchema = _dataSchema; + this._namedTypeSyntax = _namedTypeSyntax; + } + + public String docString() { + return _namedTypeSyntax.docString(); + } + + @Override + public Set modulesRequiredToUse() { + return _namedTypeSyntax.modulesRequiredToUse(); + } + + public String typeName() { + // Have to use _dataSchema.getName() instead of _template.getClassName() here because otherwise + // generics will return strings like Array instead of Array. Not sure why?? + return escapeKeyword(_dataSchema.getName(), EscapeStrategy.MANGLE); + } + + /** The type that this typeref refers to. */ + public String refTypeName() { + return createTypeSyntax(_refType()).typeName(); + } + + /** Import block for this typeref's module file */ + public String imports() { + // Gotta import the referenced type in order to compile this typeref's own module + Set refTypeImport = createTypeSyntax(_refType()).modulesRequiredToUse(); + return flattenImports(refTypeImport); + } + + // + // Private members + // + private ClassTemplateSpec _refType() { + return ClassTemplateSpec.createFromDataSchema(_dataSchema.getRef()); + } + } + + /** Create a new TyperefSyntax */ + public FlowtypeTyperefSyntax FlowtypeTyperefSyntaxCreate(TyperefTemplateSpec template) { + return new FlowtypeTyperefSyntax(template, template.getSchema(), new FlowtypeNamedTypeSyntax(template.getSchema())); + } + + /** TS syntax for the symbol of an enum */ + public class FlowtypeEnumSymbolSyntax { + private final EnumTemplateSpec _template; + private final EnumDataSchema _dataSchema; + private final String _symbolString; + + public FlowtypeEnumSymbolSyntax(EnumTemplateSpec _template, EnumDataSchema _dataSchema, String _symbolString) { + this._template = _template; + this._dataSchema = _dataSchema; + this._symbolString = _symbolString; + } + + /** + * Returns the quoted value that will be transmitted for this enum over the wire. + * + * Used to make a string-literal union representing the enum. + **/ + public String stringLiteralValue() { + return "\"" + _symbolString + "\""; + } + + /** + * Returns a variable name that can represent the enum value. Will be used to make something like + * const PINEAPPLE: Fruits = "PINEAPPLE"; + */ + public String moduleConstValue() { + return escapeKeyword(_symbolString, EscapeStrategy.MANGLE); + } + + public String docString() { + String symbolDoc = _dataSchema.getSymbolDocs().get(_symbolString); + DataMap deprecatedSymbols = (DataMap) _dataSchema.getProperties().get("deprecatedSymbols"); + Object symbolDeprecation = null; + + if (deprecatedSymbols != null) { + symbolDeprecation = deprecatedSymbols.get(_symbolString); + } + return docComment( + symbolDoc, + symbolDeprecation + ); + } + } + + /** TS syntax for enumerations. {@link FlowtypeEnumSymbolSyntax}. */ + public class FlowtypeEnumSyntax implements FlowtypeTypeSyntax { + private final EnumTemplateSpec _template; + private final EnumDataSchema _dataSchema; + private final FlowtypeNamedTypeSyntax _namedTypeSyntax; + + public FlowtypeEnumSyntax(EnumTemplateSpec _template) { + this._template = _template; + this._dataSchema = _template.getSchema(); + this._namedTypeSyntax = new FlowtypeNamedTypeSyntax(_dataSchema); + } + + public String typeName() { + return _namedTypeSyntax.typeName(); + } + + public String docString() { + return _namedTypeSyntax.docString(); + } + + /** + * Returns true in the usual case that we need a module with the same name as this type in which to house + * the enum's constants. + **/ + public boolean requiresCompanionModule() { + return this.symbols().size() > 0; + } + + /** + * Creates the string literal union for this enum. + * + * e.g. for Fruits { APPLE, ORANGE } it will produce the following valid typescript: + * + * "APPLE" | "ORANGE" + **/ + public String stringLiteralUnion() { + List symbols = this.symbols(); + if (symbols.size() == 0) { + return "void"; // Helps us compile if some bozo declared an empty union. + } else { + return this._interleaveSymbolStrings(" | "); + } + } + + /** + * Creates the typescript array literal for all values of this enum. + * e.g. for Fruits { APPLE, ORANGE } it will produce the following valid typescript: + * + * ["APPLE", "ORANGE"] + */ + public String arrayLiteral() { + return "[" + this._interleaveSymbolStrings(", ") + "]"; + } + + @Override + public Set modulesRequiredToUse() { + // Since this sucker is declared in its own file you've gotta import it to use it. + return _namedTypeSyntax.modulesRequiredToUse(); + } + + /** Syntax for all the values in this enum */ + public List symbols() { + List symbols = new ArrayList<>(); + for (String symbol : _dataSchema.getSymbols()) { + symbols.add(new FlowtypeEnumSymbolSyntax(_template, _dataSchema, symbol)); + } + return symbols; + } + + private String _interleaveSymbolStrings(String delimiter) { + List symbols = this.symbols(); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < symbols.size(); i++) { + FlowtypeEnumSymbolSyntax symbol = symbols.get(i); + boolean isLast = (i + 1 == symbols.size()); + sb.append(symbol.stringLiteralValue()); + + if (!isLast) { + sb.append(delimiter); + } + } + return sb.toString(); + } + } +} diff --git a/flowtype/generator/src/main/java/org/coursera/courier/flowtype/GlobalConfig.java b/flowtype/generator/src/main/java/org/coursera/courier/flowtype/GlobalConfig.java new file mode 100644 index 00000000..5fa7b92a --- /dev/null +++ b/flowtype/generator/src/main/java/org/coursera/courier/flowtype/GlobalConfig.java @@ -0,0 +1,47 @@ +package org.coursera.courier.flowtype; + +import com.linkedin.data.DataMap; +import com.linkedin.data.schema.DataSchema; +import com.linkedin.pegasus.generator.spec.ClassTemplateSpec; +import com.linkedin.pegasus.generator.spec.UnionTemplateSpec; + +public class GlobalConfig { + public final FlowtypeProperties defaults; + + public GlobalConfig( + FlowtypeProperties.Optionality defaultOptionality, + boolean defaultEquatable, + boolean defaultOmit) { + defaults = new FlowtypeProperties(defaultOptionality, defaultEquatable, defaultOmit); + } + + public FlowtypeProperties lookupFlowtypeProperties(ClassTemplateSpec templateSpec) { + DataSchema schema = templateSpec.getSchema(); + if (templateSpec instanceof UnionTemplateSpec && templateSpec.getOriginalTyperefSchema() != null) { + schema = templateSpec.getOriginalTyperefSchema(); + } + + if (schema == null) { + return defaults; + } else { + Object typescript = schema.getProperties().get("typescript"); + if (typescript == null || !(typescript instanceof DataMap)) { + return defaults; + } + DataMap properties = ((DataMap) typescript); + + String optionalityString = properties.getString("optionality"); + + FlowtypeProperties.Optionality optionality = + optionalityString == null ? defaults.optionality : FlowtypeProperties.Optionality.valueOf(optionalityString); + + Boolean maybeEquatable = properties.getBoolean("equatable"); + boolean equatable = maybeEquatable == null ? defaults.equatable : maybeEquatable; + + Boolean maybeOmit = properties.getBoolean("omit"); + boolean omit = maybeOmit == null ? defaults.omit : maybeOmit; + + return new FlowtypeProperties(optionality, equatable, omit); + } + } +} diff --git a/flowtype/generator/src/main/resources/runtime/CourierRuntime.js b/flowtype/generator/src/main/resources/runtime/CourierRuntime.js new file mode 100644 index 00000000..b2a5e4d9 --- /dev/null +++ b/flowtype/generator/src/main/resources/runtime/CourierRuntime.js @@ -0,0 +1,21 @@ +// @flow +// +// Copyright 2016 Coursera Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export interface Map { + [key: string]: ValueT; +} + diff --git a/flowtype/generator/src/main/resources/rythm/enum.txt b/flowtype/generator/src/main/resources/rythm/enum.txt new file mode 100644 index 00000000..f204448d --- /dev/null +++ b/flowtype/generator/src/main/resources/rythm/enum.txt @@ -0,0 +1,5 @@ +@args org.coursera.courier.flowtype.FlowtypeSyntax.FlowtypeEnumSyntax enumeration + +@import org.coursera.courier.flowtype.FlowtypeSyntax.FlowtypeEnumSymbolSyntax +@enumeration.docString() +export type @enumeration.typeName() = @enumeration.stringLiteralUnion(); diff --git a/flowtype/generator/src/main/resources/rythm/fixed.txt b/flowtype/generator/src/main/resources/rythm/fixed.txt new file mode 100644 index 00000000..6e595b9b --- /dev/null +++ b/flowtype/generator/src/main/resources/rythm/fixed.txt @@ -0,0 +1,4 @@ +@args org.coursera.courier.flowtype.FlowtypeSyntax.TSFixedSyntax fixed + +@fixed.docString() +export type @fixed.typeName() = string; diff --git a/flowtype/generator/src/main/resources/rythm/record.txt b/flowtype/generator/src/main/resources/rythm/record.txt new file mode 100644 index 00000000..aeaff898 --- /dev/null +++ b/flowtype/generator/src/main/resources/rythm/record.txt @@ -0,0 +1,20 @@ +@args org.coursera.courier.flowtype.FlowtypeSyntax.FlowtypeRecordSyntax record +@import org.coursera.courier.flowtype.FlowtypeSyntax.FlowtypeUnionSyntax +@import org.coursera.courier.flowtype.FlowtypeSyntax.FlowRecordFieldSyntax + +@record.imports() + +@record.docString() + +@if(record.requiresCompanionModule()) { + @for(FlowtypeUnionSyntax union: record.enclosedUnions()) { + @union(union) + } +} + +export type @record.typeName() = { + @for(FlowRecordFieldSyntax field: record.fields()) { + @field.docString() + @field.accessorName() @field.questionMarkIfOptional(): @field.typeName(); + } +} \ No newline at end of file diff --git a/flowtype/generator/src/main/resources/rythm/typeref.txt b/flowtype/generator/src/main/resources/rythm/typeref.txt new file mode 100644 index 00000000..33484983 --- /dev/null +++ b/flowtype/generator/src/main/resources/rythm/typeref.txt @@ -0,0 +1,6 @@ +@args org.coursera.courier.flowtype.FlowtypeSyntax.FlowtypeTyperefSyntax typeref + +@typeref.imports() + +@typeref.docString() +export type @typeref.typeName() = @typeref.refTypeName(); diff --git a/flowtype/generator/src/main/resources/rythm/union.txt b/flowtype/generator/src/main/resources/rythm/union.txt new file mode 100644 index 00000000..559209f5 --- /dev/null +++ b/flowtype/generator/src/main/resources/rythm/union.txt @@ -0,0 +1,7 @@ +@args org.coursera.courier.flowtype.FlowtypeSyntax.FlowtypeUnionSyntax union +@import org.coursera.courier.flowtype.FlowtypeSyntax.FlowtypeUnionSyntax +@import org.coursera.courier.flowtype.FlowtypeSyntax.FlowtypeUnionMemberSyntax +@union.imports() + +@union.docString() +export type @union.typeName() = @union.memberUnionTypeExpression(); \ No newline at end of file diff --git a/flowtype/testsuite/.babelrc b/flowtype/testsuite/.babelrc new file mode 100644 index 00000000..4285ecfa --- /dev/null +++ b/flowtype/testsuite/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["flow"] +} diff --git a/flowtype/testsuite/.flowconfig b/flowtype/testsuite/.flowconfig new file mode 100644 index 00000000..1fed4453 --- /dev/null +++ b/flowtype/testsuite/.flowconfig @@ -0,0 +1,11 @@ +[ignore] + +[include] + +[libs] + +[lints] + +[options] + +[strict] diff --git a/flowtype/testsuite/.gitignore b/flowtype/testsuite/.gitignore new file mode 100644 index 00000000..0b65a6bf --- /dev/null +++ b/flowtype/testsuite/.gitignore @@ -0,0 +1,6 @@ +node_modules +.tmp +typings +src/tslite-bindings +src/flowtype-bindings +npm-debug.log diff --git a/flowtype/testsuite/full-build.sh b/flowtype/testsuite/full-build.sh new file mode 100755 index 00000000..c15fd89c --- /dev/null +++ b/flowtype/testsuite/full-build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +cd $script_dir + +npm run-script full-build diff --git a/flowtype/testsuite/package.json b/flowtype/testsuite/package.json new file mode 100644 index 00000000..42b0eab9 --- /dev/null +++ b/flowtype/testsuite/package.json @@ -0,0 +1,34 @@ +{ + "name": "courier-flowtype-generator-test", + "version": "1.0.0", + "description": "Test-suite for the flowtype courier bindings", + "main": "src/index.js", + "scripts": { + "flow": "./node_modules/.bin/flow", + "setup": "npm install && npm run-script typings-install", + "compile": "./node_modules/.bin/tsc", + "typings-install": "./node_modules/.bin/typings install", + "test": "echo done", + "full-build": "npm run-script setup && npm run-script test" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/coursera/courier.git" + }, + "author": "Kyle Verhoog & Moaaz Sidat", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/coursera/courier/issues" + }, + "homepage": "https://github.com/coursera/courier#readme", + "dependencies": { + "jasmine": "2.4.1", + "typescript": "1.8.9", + "typings": "0.7.10" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-preset-flow": "^6.23.0", + "flow-bin": "^0.58.0" + } +} diff --git a/flowtype/testsuite/spec/support/jasmine.json b/flowtype/testsuite/spec/support/jasmine.json new file mode 100644 index 00000000..901fcf15 --- /dev/null +++ b/flowtype/testsuite/spec/support/jasmine.json @@ -0,0 +1,8 @@ +{ + "spec_dir": ".tmp/spec", + "spec_files": [ + "**/*[sS]pec.js" + ], + "stopSpecOnExpectationFailure": false, + "random": false +} diff --git a/flowtype/testsuite/src/compilation-failures/array_bad-item-type-enum-expected.ts b/flowtype/testsuite/src/compilation-failures/array_bad-item-type-enum-expected.ts new file mode 100644 index 00000000..bfb65315 --- /dev/null +++ b/flowtype/testsuite/src/compilation-failures/array_bad-item-type-enum-expected.ts @@ -0,0 +1,6 @@ +import {WithRecordArray} from "./tslite-bindings/org.coursera.arrays.WithRecordArray"; + +const wra: WithRecordArray = { + "empties" : [ { }, { }, { } ], + "fruits" : [ "APPLE", "BANANA", "ORANGE", "BUZZSAW"] // oops a buzzsaw is not a fruit +}; diff --git a/flowtype/testsuite/src/compilation-failures/array_bad-item-type.ts b/flowtype/testsuite/src/compilation-failures/array_bad-item-type.ts new file mode 100644 index 00000000..d45c8fb5 --- /dev/null +++ b/flowtype/testsuite/src/compilation-failures/array_bad-item-type.ts @@ -0,0 +1,12 @@ +import {WithPrimitivesArray} from "./tslite-bindings/org.coursera.arrays.WithPrimitivesArray"; + +const a: WithPrimitivesArray = { + "bytes" : [ "\u0000\u0001\u0002", + "\u0003\u0004\u0005" ], + "longs" : [ 10, 20, 30 ], + "strings" : [ "a", "b", "c" ], + "doubles" : [ 11.1, 22.2, 33.3 ], + "booleans" : [ false, true ], + "floats" : [ 1.1, 2.2, 3.3 ], + "ints" : [ "1", "2", "3" ] // oops! these should be numbers +}; diff --git a/flowtype/testsuite/src/compilation-failures/enum_bad-string.ts b/flowtype/testsuite/src/compilation-failures/enum_bad-string.ts new file mode 100644 index 00000000..a605d1f0 --- /dev/null +++ b/flowtype/testsuite/src/compilation-failures/enum_bad-string.ts @@ -0,0 +1,3 @@ +import {Fruits} from "./tslite-bindings/org.coursera.enums.Fruits"; + +const a: Fruits = "BUZZSAW"; // valid objects are only APPLE, BANANA, ORANGE, PINEAPPLE. diff --git a/flowtype/testsuite/src/compilation-failures/flowtype-bindings b/flowtype/testsuite/src/compilation-failures/flowtype-bindings new file mode 120000 index 00000000..334e6edf --- /dev/null +++ b/flowtype/testsuite/src/compilation-failures/flowtype-bindings @@ -0,0 +1 @@ +../flowtype-bindings/ \ No newline at end of file diff --git a/flowtype/testsuite/src/compilation-failures/map_bad-value-type.ts b/flowtype/testsuite/src/compilation-failures/map_bad-value-type.ts new file mode 100644 index 00000000..39703fb8 --- /dev/null +++ b/flowtype/testsuite/src/compilation-failures/map_bad-value-type.ts @@ -0,0 +1,39 @@ +import {WithPrimitivesMap} from "./tslite-bindings/org.coursera.maps.WithPrimitivesMap"; + +const wpm: WithPrimitivesMap = { + "bytes" : { + "b" : "\u0003\u0004\u0005", + "c" : "\u0006\u0007\b", + "a" : "\u0000\u0001\u0002" + }, + "longs" : { + "b" : 20, + "c" : 30, + "a" : "what am I doing here?" // oops! not a number + }, + "strings" : { + "b" : "string2", + "c" : "string3", + "a" : "string1" + }, + "doubles" : { + "b" : 22.2, + "c" : 33.3, + "a" : 11.1 + }, + "booleans" : { + "b" : false, + "c" : true, + "a" : true + }, + "floats" : { + "b" : 2.2, + "c" : 3.3, + "a" : 1.1 + }, + "ints" : { + "b" : 2, + "c" : 3, + "a" : 1 + } +}; diff --git a/flowtype/testsuite/src/compilation-failures/record_wrong-field-type.ts b/flowtype/testsuite/src/compilation-failures/record_wrong-field-type.ts new file mode 100644 index 00000000..7d59088f --- /dev/null +++ b/flowtype/testsuite/src/compilation-failures/record_wrong-field-type.ts @@ -0,0 +1,6 @@ +import {Message} from "../expected-successes/tslite-bindings/org.coursera.records.Message"; + +const a: Message = { + "title": [], // should be a string + "body": {} // should be a string +} diff --git a/flowtype/testsuite/src/compilation-failures/typeref_wrong-type.ts b/flowtype/testsuite/src/compilation-failures/typeref_wrong-type.ts new file mode 100644 index 00000000..abee8c97 --- /dev/null +++ b/flowtype/testsuite/src/compilation-failures/typeref_wrong-type.ts @@ -0,0 +1,3 @@ +import {CustomInt} from "./tslite-bindings/org.coursera.customtypes.CustomInt"; + +const a: CustomInt = "oops im not an int"; diff --git a/flowtype/testsuite/src/compilation-failures/union_bad-body-content.ts b/flowtype/testsuite/src/compilation-failures/union_bad-body-content.ts new file mode 100644 index 00000000..b285e93b --- /dev/null +++ b/flowtype/testsuite/src/compilation-failures/union_bad-body-content.ts @@ -0,0 +1,8 @@ +import { Union } from "./tslite-bindings/org.coursera.typerefs.Union"; + +const a: Union = { + "org.coursera.records.Messag": { + "titl": "title", // should be "title" not "titl" + "body": "Hello, Courier." + } +}; diff --git a/flowtype/testsuite/src/compilation-failures/union_bad-lookup-string.ts b/flowtype/testsuite/src/compilation-failures/union_bad-lookup-string.ts new file mode 100644 index 00000000..36e0ece8 --- /dev/null +++ b/flowtype/testsuite/src/compilation-failures/union_bad-lookup-string.ts @@ -0,0 +1,8 @@ +import { Union } from "./tslite-bindings/org.coursera.typerefs.Union"; + +const a: Union = { + "org.coursera.records.Messag": { // should be "Message" not "Messag" + "title": "title", + "body": "Hello, Courier." + } +}; diff --git a/flowtype/testsuite/src/expected-successes/flowtype-bindings b/flowtype/testsuite/src/expected-successes/flowtype-bindings new file mode 120000 index 00000000..334e6edf --- /dev/null +++ b/flowtype/testsuite/src/expected-successes/flowtype-bindings @@ -0,0 +1 @@ +../flowtype-bindings/ \ No newline at end of file diff --git a/flowtype/testsuite/src/expected-successes/spec/bindings.spec.js b/flowtype/testsuite/src/expected-successes/spec/bindings.spec.js new file mode 100644 index 00000000..19d7ad01 --- /dev/null +++ b/flowtype/testsuite/src/expected-successes/spec/bindings.spec.js @@ -0,0 +1,623 @@ +import type { WithoutNamespace } from "../flowtype-bindings/.WithoutNamespace"; +import type { Map } from "../flowtype-bindings/CourierRuntime"; +import type { WithCustomArrayTestId } from "../flowtype-bindings/org.coursera.arrays.WithCustomArrayTestId"; +import type { WithCustomTypesArray } from "../flowtype-bindings/org.coursera.arrays.WithCustomTypesArray"; +import type { WithCustomTypesArrayUnion } from "../flowtype-bindings/org.coursera.arrays.WithCustomTypesArrayUnion"; +import type { WithPrimitivesArray } from "../flowtype-bindings/org.coursera.arrays.WithPrimitivesArray"; +import type { WithRecordArray } from "../flowtype-bindings/org.coursera.arrays.WithRecordArray"; +import type { BooleanId } from "../flowtype-bindings/org.coursera.customtypes.BooleanId"; +import type { BoxedIntId } from "../flowtype-bindings/org.coursera.customtypes.BoxedIntId"; +import type { ByteId } from "../flowtype-bindings/org.coursera.customtypes.ByteId"; +import type { CaseClassCustomIntWrapper } from "../flowtype-bindings/org.coursera.customtypes.CaseClassCustomIntWrapper"; +import type { CaseClassStringIdWrapper } from "../flowtype-bindings/org.coursera.customtypes.CaseClassStringIdWrapper"; +import type { CharId } from "../flowtype-bindings/org.coursera.customtypes.CharId"; +import type { CustomArrayTestId } from "../flowtype-bindings/org.coursera.customtypes.CustomArrayTestId"; +import type { CustomInt } from "../flowtype-bindings/org.coursera.customtypes.CustomInt"; +import type { CustomIntWrapper } from "../flowtype-bindings/org.coursera.customtypes.CustomIntWrapper"; +import type { CustomMapTestKeyId } from "../flowtype-bindings/org.coursera.customtypes.CustomMapTestKeyId"; +import type { CustomMapTestValueId } from "../flowtype-bindings/org.coursera.customtypes.CustomMapTestValueId"; +import type { CustomRecord } from "../flowtype-bindings/org.coursera.customtypes.CustomRecord"; +import type { CustomRecordTestId } from "../flowtype-bindings/org.coursera.customtypes.CustomRecordTestId"; +import type { CustomUnionTestId } from "../flowtype-bindings/org.coursera.customtypes.CustomUnionTestId"; +import type { DateTime } from "../flowtype-bindings/org.coursera.customtypes.DateTime"; +import type { DoubleId } from "../flowtype-bindings/org.coursera.customtypes.DoubleId"; +import type { FloatId } from "../flowtype-bindings/org.coursera.customtypes.FloatId"; +import type { IntId } from "../flowtype-bindings/org.coursera.customtypes.IntId"; +import type { LongId } from "../flowtype-bindings/org.coursera.customtypes.LongId"; +import type { ShortId } from "../flowtype-bindings/org.coursera.customtypes.ShortId"; +import type { StringId } from "../flowtype-bindings/org.coursera.customtypes.StringId"; +import type { DeprecatedRecord } from "../flowtype-bindings/org.coursera.deprecated.DeprecatedRecord"; +import type { EmptyEnum } from "../flowtype-bindings/org.coursera.enums.EmptyEnum"; +import type { EnumProperties } from "../flowtype-bindings/org.coursera.enums.EnumProperties"; +import type { Fruits } from "../flowtype-bindings/org.coursera.enums.Fruits"; +import type { DefaultLiteralEscaping } from "../flowtype-bindings/org.coursera.escaping.DefaultLiteralEscaping"; +import type { KeywordEscaping } from "../flowtype-bindings/org.coursera.escaping.KeywordEscaping"; +import type { ReservedClassFieldEscaping } from "../flowtype-bindings/org.coursera.escaping.ReservedClassFieldEscaping"; +import type { class$ } from "../flowtype-bindings/org.coursera.escaping.class"; +import type { WithFixed8 } from "../flowtype-bindings/org.coursera.fixed.WithFixed8"; +import type { Toggle } from "../flowtype-bindings/org.coursera.maps.Toggle"; +import type { WithComplexTypesMap } from "../flowtype-bindings/org.coursera.maps.WithComplexTypesMap"; +import type { WithComplexTypesMapUnion } from "../flowtype-bindings/org.coursera.maps.WithComplexTypesMapUnion"; +import type { WithCustomMapTestIds } from "../flowtype-bindings/org.coursera.maps.WithCustomMapTestIds"; +import type { WithCustomTypesMap } from "../flowtype-bindings/org.coursera.maps.WithCustomTypesMap"; +import type { WithPrimitivesMap } from "../flowtype-bindings/org.coursera.maps.WithPrimitivesMap"; +import type { WithTypedKeyMap } from "../flowtype-bindings/org.coursera.maps.WithTypedKeyMap"; +import type { CourierFile } from "../flowtype-bindings/org.coursera.records.CourierFile"; +import type { JsonTest } from "../flowtype-bindings/org.coursera.records.JsonTest"; +import type { Message } from "../flowtype-bindings/org.coursera.records.Message"; +import type { WithAnonymousUnionArray } from "../flowtype-bindings/org.coursera.arrays.WithAnonymousUnionArray"; +import type { Note } from "../flowtype-bindings/org.coursera.records.Note"; +import type { WithDateTime } from "../flowtype-bindings/org.coursera.records.WithDateTime"; +import type { WithFlatTypedDefinition } from "../flowtype-bindings/org.coursera.records.WithFlatTypedDefinition"; +import type { WithInclude } from "../flowtype-bindings/org.coursera.records.WithInclude"; +import type { WithTypedDefinition } from "../flowtype-bindings/org.coursera.records.WithTypedDefinition"; +import type { WithUnion } from "../flowtype-bindings/org.coursera.records.WithUnion"; +import type { Fixed8 } from "../flowtype-bindings/org.coursera.fixed.Fixed8"; +import type { class$ as EscapedClassRecord} from "../flowtype-bindings/org.coursera.records.class"; +import type { Simple } from "../flowtype-bindings/org.coursera.records.primitivestyle.Simple"; +import type { WithComplexTypes } from "../flowtype-bindings/org.coursera.records.primitivestyle.WithComplexTypes"; +import type { WithPrimitives } from "../flowtype-bindings/org.coursera.records.primitivestyle.WithPrimitives"; +import type { BooleanTyperef } from "../flowtype-bindings/org.coursera.records.test.BooleanTyperef"; +import type { BytesTyperef } from "../flowtype-bindings/org.coursera.records.test.BytesTyperef"; +import type { DoubleTyperef } from "../flowtype-bindings/org.coursera.records.test.DoubleTyperef"; +import type { Empty } from "../flowtype-bindings/org.coursera.records.test.Empty"; +import type { FloatTyperef } from "../flowtype-bindings/org.coursera.records.test.FloatTyperef"; +import type { InlineOptionalRecord } from "../flowtype-bindings/org.coursera.records.test.InlineOptionalRecord"; +import type { InlineRecord } from "../flowtype-bindings/org.coursera.records.test.InlineRecord"; +import type { IntCustomType as TestIntCustomType } from "../flowtype-bindings/org.coursera.records.test.IntCustomType"; +import type { IntTyperef as TestIntTyperef } from "../flowtype-bindings/org.coursera.records.test.IntTyperef"; +import type { LongTyperef } from "../flowtype-bindings/org.coursera.records.test.LongTyperef"; +import type { Message as TestMessage } from "../flowtype-bindings/org.coursera.records.test.Message"; +import type { NumericDefaults } from "../flowtype-bindings/org.coursera.records.test.NumericDefaults"; +import type { OptionalBooleanTyperef } from "../flowtype-bindings/org.coursera.records.test.OptionalBooleanTyperef"; +import type { OptionalBytesTyperef } from "../flowtype-bindings/org.coursera.records.test.OptionalBytesTyperef"; +import type { OptionalDoubleTyperef } from "../flowtype-bindings/org.coursera.records.test.OptionalDoubleTyperef"; +import type { OptionalFloatTyperef } from "../flowtype-bindings/org.coursera.records.test.OptionalFloatTyperef"; +import type { OptionalIntCustomType } from "../flowtype-bindings/org.coursera.records.test.OptionalIntCustomType"; +import type { OptionalIntTyperef } from "../flowtype-bindings/org.coursera.records.test.OptionalIntTyperef"; +import type { OptionalLongTyperef } from "../flowtype-bindings/org.coursera.records.test.OptionalLongTyperef"; +import type { OptionalStringTyperef } from "../flowtype-bindings/org.coursera.records.test.OptionalStringTyperef"; +import type { RecursivelyDefinedRecord } from "../flowtype-bindings/org.coursera.records.test.RecursivelyDefinedRecord"; +import type { Simple as TestSimple } from "../flowtype-bindings/org.coursera.records.test.Simple"; +import type { StringTyperef } from "../flowtype-bindings/org.coursera.records.test.StringTyperef"; +import type { With22Fields } from "../flowtype-bindings/org.coursera.records.test.With22Fields"; +import type { With23Fields } from "../flowtype-bindings/org.coursera.records.test.With23Fields"; +import type { WithCaseClassCustomType } from "../flowtype-bindings/org.coursera.records.test.WithCaseClassCustomType"; +import type { WithComplexTypeDefaults } from "../flowtype-bindings/org.coursera.records.test.WithComplexTypeDefaults"; +import type { WithComplexTyperefs } from "../flowtype-bindings/org.coursera.records.test.WithComplexTyperefs"; +import type { WithComplexTypes as TestWithComplexTypes } from "../flowtype-bindings/org.coursera.records.test.WithComplexTypes"; +import type { WithCourierFile } from "../flowtype-bindings/org.coursera.records.test.WithCourierFile"; +import type { WithCustomIntWrapper } from "../flowtype-bindings/org.coursera.records.test.WithCustomIntWrapper"; +import type { WithCustomRecord } from "../flowtype-bindings/org.coursera.records.test.WithCustomRecord"; +import type { WithCustomRecordTestId } from "../flowtype-bindings/org.coursera.records.test.WithCustomRecordTestId"; +import type { WithDateTime as TestWithDateTime } from "../flowtype-bindings/org.coursera.records.test.WithDateTime"; +import type { WithInclude as TestWithInclude } from "../flowtype-bindings/org.coursera.records.test.WithInclude"; +import type { WithInlineRecord } from "../flowtype-bindings/org.coursera.records.test.WithInlineRecord"; +import type { WithOmitField } from "../flowtype-bindings/org.coursera.records.test.WithOmitField"; +import type { WithOptionalComplexTypeDefaults } from "../flowtype-bindings/org.coursera.records.test.WithOptionalComplexTypeDefaults"; +import type { WithOptionalComplexTypes } from "../flowtype-bindings/org.coursera.records.test.WithOptionalComplexTypes"; +import type { WithOptionalComplexTypesDefaultNone } from "../flowtype-bindings/org.coursera.records.test.WithOptionalComplexTypesDefaultNone"; +import type { WithOptionalPrimitiveCustomTypes } from "../flowtype-bindings/org.coursera.records.test.WithOptionalPrimitiveCustomTypes"; +import type { WithOptionalPrimitiveDefaultNone } from "../flowtype-bindings/org.coursera.records.test.WithOptionalPrimitiveDefaultNone"; +import type { WithOptionalPrimitiveDefaults } from "../flowtype-bindings/org.coursera.records.test.WithOptionalPrimitiveDefaults"; +import type { WithOptionalPrimitiveTyperefs } from "../flowtype-bindings/org.coursera.records.test.WithOptionalPrimitiveTyperefs"; +import type { WithOptionalPrimitives } from "../flowtype-bindings/org.coursera.records.test.WithOptionalPrimitives"; +import type { WithPrimitiveCustomTypes } from "../flowtype-bindings/org.coursera.records.test.WithPrimitiveCustomTypes"; +import type { WithPrimitiveDefaults } from "../flowtype-bindings/org.coursera.records.test.WithPrimitiveDefaults"; +import type { WithPrimitiveTyperefs } from "../flowtype-bindings/org.coursera.records.test.WithPrimitiveTyperefs"; +import type { WithPrimitives as TestWithPrimitives } from "../flowtype-bindings/org.coursera.records.test.WithPrimitives"; +import type { WithUnionWithInlineRecord } from "../flowtype-bindings/org.coursera.records.test.WithUnionWithInlineRecord"; +import type { ArrayTyperef } from "../flowtype-bindings/org.coursera.typerefs.ArrayTyperef"; +import type { EnumTyperef } from "../flowtype-bindings/org.coursera.typerefs.EnumTyperef"; +import type { FlatTypedDefinition } from "../flowtype-bindings/org.coursera.typerefs.FlatTypedDefinition"; +import type { InlineRecord as InlineRecordTypeRef } from "../flowtype-bindings/org.coursera.typerefs.InlineRecord"; +import type { InlineRecord2 } from "../flowtype-bindings/org.coursera.typerefs.InlineRecord2"; +import type { IntTyperef } from "../flowtype-bindings/org.coursera.typerefs.IntTyperef"; +import type { MapTyperef } from "../flowtype-bindings/org.coursera.typerefs.MapTyperef"; +import type { RecordTyperef } from "../flowtype-bindings/org.coursera.typerefs.RecordTyperef"; +import type { TypedDefinition } from "../flowtype-bindings/org.coursera.typerefs.TypedDefinition"; +import type { Union } from "../flowtype-bindings/org.coursera.typerefs.Union"; +import type { UnionTyperef } from "../flowtype-bindings/org.coursera.typerefs.UnionTyperef"; +import type { UnionWithInlineRecord } from "../flowtype-bindings/org.coursera.typerefs.UnionWithInlineRecord"; +import type { IntCustomType } from "../flowtype-bindings/org.coursera.unions.IntCustomType"; +import type { IntTyperef as IntTyperefUnion} from "../flowtype-bindings/org.coursera.unions.IntTyperef"; +import type { WithComplexTypesUnion } from "../flowtype-bindings/org.coursera.unions.WithComplexTypesUnion"; +import type { WithCustomUnionTestId } from "../flowtype-bindings/org.coursera.unions.WithCustomUnionTestId"; +import type { WithEmptyUnion } from "../flowtype-bindings/org.coursera.unions.WithEmptyUnion"; +import type { WithPrimitiveCustomTypesUnion } from "../flowtype-bindings/org.coursera.unions.WithPrimitiveCustomTypesUnion"; +import type { WithPrimitiveTyperefsUnion } from "../flowtype-bindings/org.coursera.unions.WithPrimitiveTyperefsUnion"; +import type { WithRecordCustomTypeUnion } from "../flowtype-bindings/org.coursera.unions.WithRecordCustomTypeUnion"; +import type { Fortune } from "../flowtype-bindings/org.example.Fortune"; +import type { FortuneCookie } from "../flowtype-bindings/org.example.FortuneCookie"; +import type { FortuneTelling } from "../flowtype-bindings/org.example.FortuneTelling"; +import type { MagicEightBall } from "../flowtype-bindings/org.example.MagicEightBall"; +import type { MagicEightBallAnswer } from "../flowtype-bindings/org.example.MagicEightBallAnswer"; +import type { TyperefExample } from "../flowtype-bindings/org.example.TyperefExample"; +import type { DateTime as CommonDateTime } from "../flowtype-bindings/org.example.common.DateTime"; +import type { Timestamp } from "../flowtype-bindings/org.example.common.Timestamp"; +import type { DateTime as OtherDateTime } from "../flowtype-bindings/org.example.other.DateTime"; +import type { record } from "../flowtype-bindings/org.example.record"; +import type { WithPrimitivesUnion } from "../flowtype-bindings/org.coursera.unions.WithPrimitivesUnion"; +import type * as ts from "typescript"; +const fs = require('fs'); + +import type CustomMatcherFactories = jasmine.CustomMatcherFactories; +import type CompilerOptions = ts.CompilerOptions; +import type TranspileOptions = ts.TranspileOptions; +import type Diagnostic = ts.Diagnostic; + +const flowPragmaComment = "/* @flow */"; + +// Add a jasmine matcher that will attempt to compile a ts file and report +// any compilation errors +const toCompileMatcher: CustomMatcherFactories = { + toCompile: (util: any, customEqualityTesters: any) => { + return { + compare: (fileName: any, message:any) => { + const result: any = {}; + var compilerOptions: CompilerOptions = { + project: "/Users/eboto/code/courier/typescript-lite/testsuite/tsconfig.json", + diagnostics: true + }; + + const program = ts.createProgram( + [fileName], + compilerOptions + ); + + const errors = program.getGlobalDiagnostics() + .concat(program.getSemanticDiagnostics()) + .concat(program.getDeclarationDiagnostics()) + .concat(program.getSyntacticDiagnostics()); + const errorStr = errors.reduce((accum: any, err: Diagnostic) => { + const errFile = err.file; + const msgText = ts.flattenDiagnosticMessageText(err.messageText, "\n"); + const nextAccum = accum + `\n${errFile.path}:${errFile.pos}\n${msgText}\n`; + return nextAccum; + }, ""); + + result.pass = (errors.length == 0); + if (!result.pass) { + result.message = `Compilation expectation failed: ${message} Error was: ${errorStr}`; + } + + return result; + } + }; + } +}; + +// +// Only test the runtime behavior of Unions +// +describe("Unions", () => { + it("should compile from correct javascript and unpack", () => { + const unionOfMessage: WithUnion = { + "value": { + "org.coursera.records.Message": { + "title": "title", + "body": "Hello, Courier." + } + } + }; + const {note, message} = Union.unpack(unionOfMessage.value); + expect(note).toBeUndefined(); + expect(message).not.toBeUndefined(); + expect(message.title).toBe("title"); + expect(message.body).toBe("Hello, Courier."); + }); + + it("should access all primitive unions properly", () => { + const keyShouldNotBeUndefined = (correctUnionKey: string) => (withUnion: WithPrimitivesUnion) => { + const union = withUnion.union; + const keys = Object.keys(union); + keys.forEach((key) => { + if (key == correctUnionKey) { + expect(union[key]).not.toBeUndefined(`Expected '${key}' not to be undefined in ${JSON.stringify(union)}`) + } else { + expect(union[key]).toBeUndefined(`Expected '${key}' to be defined in ${JSON.stringify(union)}. Only '${correctUnionKey}' was supposed to be defined.`); + } + }); + }; + const expectations = [ + [wpu_int, keyShouldNotBeUndefined("int")], + [wpu_long, keyShouldNotBeUndefined("long")], + [wpu_float, keyShouldNotBeUndefined("float")], + [wpu_double, keyShouldNotBeUndefined("double")], + [wpu_bool, keyShouldNotBeUndefined("boolean")], + [wpu_string, keyShouldNotBeUndefined("string")], + [wpu_bytes, keyShouldNotBeUndefined("bytes")] + ] + + expectations.forEach((expectationData) => { + const [unionInstance, expectation] = expectationData; + (expectation as any)(unionInstance); + }); + }); +}); + +describe("Enums", () => { + it("Should have successful accessors", () => { + const fruit1: Fruits = "APPLE"; + // expect(fruit1).toEqual(Fruits.APPLE); + // expect(fruit1 == Fruits.APPLE).toBe(true); + }); + + it("Should have nice switch/case semantics", () => { + const fruit1: Fruits = "APPLE"; + let result: string; + switch (fruit1) { + case "APPLE": + result = "It was an apple"; + break; + case "PEAR": + result = "It was a pear"; + break; + default: + result = "I don't know what it was."; + } + + expect(result).toEqual("It was an apple"); + result = "It's still an apple"; + // switch (fruit1) { + // case Fruits.APPLE: + // result = "It's still an apple"; + // break; + // default: + // result = "Something else." + // } + + expect(result).toEqual("It's still an apple"); + }); + + it("Should transcribe both the class-level and symbol-level documentation from the courier spec", () => { + const fruitsFile = fs.readFileSync("src/flowtype-bindings/org.coursera.enums.Fruits.ts").toString(); + const typeComment = "An enum dedicated to the finest of the food groups."; + + expect(fruitsFile).toContain(flowPragmaComment); + expect(fruitsFile).toContain(typeComment); + }); + + // it("Should be enumerated with the .all function", () => { + // expect(Fruits.all).toEqual(["APPLE", "BANANA", "ORANGE", "PINEAPPLE"]); + // }); +}); + + +// +// Now just declare a bunch of JSON types (sourced from courier/reference-suite/src/main/json. +// +// Compilation will fail if generation failed in compatibility with these known-good json types. +// +const customint: CustomInt = 1; // typerefs should work + +const boolid: BooleanId = true; +const byteid: ByteId = "bytes just a string baby!"; +const ref_of_a_ref: CustomIntWrapper = 1; +const fortune_fortuneCookie: Fortune = { + "telling": { + "org.example.FortuneCookie": { + "message": " a message", + "certainty": 0.1, + "luckyNumbers": [1, 2, 3] + } + }, + "createdAt": "2015-01-01T00:00:00.000Z" +}; + +const fortune_magicEightBall: Fortune = { + "telling": { + "org.example.MagicEightBall": { + "question": "A question", + "answer": "IT_IS_CERTAIN" + } + }, + "createdAt": "2015-01-01T00:00:00.000Z" +}; + +const fortuneCookie: FortuneCookie = { + "message": " a message", + "certainty": 0.1, + "luckyNumbers": [1, 2, 3] +}; + +const fortuneCookie_lackingOptional: FortuneCookie = { + "message": "a message", + "luckyNumbers": [1, 2, 3] +}; + +const kw_escaping: KeywordEscaping = { + "type" : "test" +}; + +const msg: Message = { + "title": "example title", + "body": "example body" +}; + +const rcfe: ReservedClassFieldEscaping = { + "data" : "dataText", + "schema": "schemaText", + "copy": "copyText", + "clone": "cloneText" +}; + +const simple: Simple = { "message": "simple message" }; + +const withComplexTypes: TestWithComplexTypes = { + "record": { "message": "record"}, + "enum": "APPLE", + "union": { "org.coursera.records.test.Simple": { "message": "union" }}, + "array": [1, 2], + "map": { "a": 1, "b": 2}, + "complexMap": { "x": { "message": "complexMap"}}, + "custom": 100 +}; + +const wu: WithUnion = { + "value": { + "org.coursera.records.Message": { + "title": "title", + "body": "Hello, Courier." + } + } +}; + +const wctu_empty: WithComplexTypesUnion = { + "union" : { + "org.coursera.records.test.Empty" : { } + } +}; + +const wctu_enum: WithComplexTypesUnion = { + "union" : { + "org.coursera.enums.Fruits" : "APPLE" + } +}; + +const withCustomTypesArr: WithCustomTypesArray = { + "ints" : [ 1, 2, 3 ], + "arrays": [ [ { "message": "a1" } ] ], + "maps": [ { "a": { "message": "m1" } } ], + "unions": [ + { "int": 1 }, + { "string": "str" }, + { "org.coursera.records.test.Simple": { "message": "u1" }} + ], + "fixed": [ "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007" ] +}; + +const wctm: WithCustomTypesMap = { + "ints" : { + "b" : 2, + "c" : 3, + "a" : 1 + } +}; + +const wctm2: WithComplexTypesMap = { + "empties" : { + "b" : { }, + "c" : { }, + "a" : { } + }, + "fruits" : { + "b" : "BANANA", + "c" : "ORANGE", + "a" : "APPLE" + }, + "arrays" : { + "a": [ {"message": "v1"}, {"message": "v2"} ] + }, + "maps": { + "o1": { + "i1": { "message": "o1i1" }, + "i2": { "message": "o1i2" } + } + }, + "unions": { + "a": { "int": 1 }, + "b": { "string": "u1" } + }, + "fixed": { + "a": "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007" + } +}; + +const wdt: TestWithDateTime = { + "createdAt": 1420070400000 +}; + +const wp1: WithPrimitiveCustomTypes = { + "intField" : 1 +}; + +const wpu: WithPrimitiveCustomTypesUnion = { + "union" : { + "int" : 1 + } +}; + +const wp2: WithPrimitives = { + "floatField" : 3.3, + "doubleField" : 4.4, + "intField" : 1, + "bytesField" : "\u0000\u0001\u0002", + "longField" : 2, + "booleanField" : true, + "stringField" : "str" +}; + +const wpa: WithPrimitivesArray = { + "bytes" : [ "\u0000\u0001\u0002", + "\u0003\u0004\u0005" ], + "longs" : [ 10, 20, 30 ], + "strings" : [ "a", "b", "c" ], + "doubles" : [ 11.1, 22.2, 33.3 ], + "booleans" : [ false, true ], + "floats" : [ 1.1, 2.2, 3.3 ], + "ints" : [ 1, 2, 3 ] +}; + +const wpm: WithPrimitivesMap = { + "bytes" : { + "b" : "\u0003\u0004\u0005", + "c" : "\u0006\u0007\b", + "a" : "\u0000\u0001\u0002" + }, + "longs" : { + "b" : 20, + "c" : 30, + "a" : 10 + }, + "strings" : { + "b" : "string2", + "c" : "string3", + "a" : "string1" + }, + "doubles" : { + "b" : 22.2, + "c" : 33.3, + "a" : 11.1 + }, + "booleans" : { + "b" : false, + "c" : true, + "a" : true + }, + "floats" : { + "b" : 2.2, + "c" : 3.3, + "a" : 1.1 + }, + "ints" : { + "b" : 2, + "c" : 3, + "a" : 1 + } +}; + + + +const wtkm: WithTypedKeyMap = { + "ints" : { "1": "int" }, + "longs" : { "2": "long" }, + "floats" : { "3.14": "float" }, + "doubles" : { "2.71": "double" }, + "booleans" : { "true": "boolean" }, + "strings" : { "key": "string" }, + "bytes" : { "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007": "bytes" }, + "record" : { "(message~key)": "record" }, + "array" : { "List(1,2)": "array" }, + "enum" : { "APPLE": "enum" }, + "custom" : { "100": "custom" }, + "fixed" : { "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007": "fixed" } +}; + +const wra: WithRecordArray = { + "empties" : [ { }, { }, { } ], + "fruits" : [ "APPLE", "BANANA", "ORANGE" ] +}; + +const wctu_array: WithComplexTypesUnion = { + "union" : { + "array" : [ { "message": "a1" } ] // TODO(eboto): Oops! Looks like it specified this in TS like arraySimple: union["Array"]. It should have just been "array" + } +}; + +const wctu_map: WithComplexTypesUnion = { + "union" : { + "map" : { "a": { "message": "m1" } } + } +}; + + +const wpu_long: WithPrimitivesUnion = { + "union" : { + "long" : 2 + } +}; + +const wpu_bool: WithPrimitivesUnion = { + "union" : { + "boolean" : true + } +}; + +const wpu_string: WithPrimitivesUnion = { + "union" : { + "string" : "thestring" + } +}; + +const wpu_bytes: WithPrimitivesUnion = { + "union" : { + "bytes" : "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007" + } +}; + + +const wpu_str: WithPrimitivesUnion = { + "union" : { + "string" : "str" + } +}; + +const wpu_int: WithPrimitivesUnion = { + "union" : { + "int" : 1 + } +}; + +const wpu_float: WithPrimitivesUnion = { + "union" : { + "float" : 3.0 + } +}; + +const wpu_double: WithPrimitivesUnion = { + "union" : { + "double" : 4.0 + } +}; + + + + +/* TODO(eboto): This one fails. Why? What is a TypedDefinition? +const wtd: WithTypedDefinition = { + + "value": { + "typeName": "message", + "definition": { + "title": "title", + "body": "Hello, Courier." + } + } +}; +*/ + + +/** TODO(eboto): Uncomment after support for flat type definitions + const wftd: WithFlatTypedDefinition = { + "value": { + "typeName": "message", + "title": "title", + "body": "Hello, Courier." + } +}; + */ + +/* TODO(eboto): This is not working because org.coursera.records.mutable.Simple doesn't exist. Ask jpbetz or saeta if this is actually meant to work. + const withCustomTypesArrMutable: WithCustomTypesArray = { + "ints" : [ 1, 2, 3 ], + "arrays": [ [ { "message": "a1" } ] ], + "maps": [ { "a": { "message": "m1" } } ], + "unions": [ + { "number": 1 }, + { "string": "str" }, + { "org.coursera.records.mutable.Simple": { "message": "u1" }} + ], + "fixed": [ "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007" ] + } + */ diff --git a/flowtype/testsuite/src/expected-successes/typescript-compiler.d.ts b/flowtype/testsuite/src/expected-successes/typescript-compiler.d.ts new file mode 100644 index 00000000..a3a929c2 --- /dev/null +++ b/flowtype/testsuite/src/expected-successes/typescript-compiler.d.ts @@ -0,0 +1,10 @@ +declare module "typescript-compiler" { // hilariously, no typings exist for the typescript-compiler package so we have to make our own! + export function compileString(input:string, tscArgs?:any, options?:any, onError?:(diag: any) => any): string; + export function compileStrings(input:any, tscArgs?:any, options?:any, onError?:(diag: any) => any): string; +} + +declare module jasmine { + export interface Matchers { + toCompile(errMsg: string): boolean; + } +} diff --git a/flowtype/testsuite/tsconfig.json b/flowtype/testsuite/tsconfig.json new file mode 100644 index 00000000..5914636c --- /dev/null +++ b/flowtype/testsuite/tsconfig.json @@ -0,0 +1,45 @@ +{ + "version": "1.8.7", + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "declaration": false, + "jsx": "react", + "noImplicitAny": true, + "noImplicitReturns": true, + "suppressImplicitAnyIndexErrors": true, + "removeComments": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noEmitOnError": false, + "preserveConstEnums": true, + "inlineSources": false, + "sourceMap": false, + "outDir": "./.tmp", + "rootDir": "./src/expected-successes", + "moduleResolution": "node", + "listFiles": false + }, + "formatCodeOptions": { + "indentSize": 2, + "tabSize": 4, + "newLineCharacter": "\n", + "convertTabsToSpaces": true, + "insertSpaceAfterCommaDelimiter": true, + "insertSpaceAfterSemicolonInForStatements": true, + "insertSpaceBeforeAndAfterBinaryOperators": true, + "insertSpaceAfterKeywordsInControlFlowStatements": true, + "insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, + "placeOpenBraceOnNewLineForFunctions": false, + "placeOpenBraceOnNewLineForControlBlocks": false + }, + "exclude": [ + "node_modules", + "jspm_packages", + "typings/browser", + "typings/browser.d.ts", + "src/compilation-failures", + "src/flowtype-bindings" + ] +} diff --git a/flowtype/testsuite/typings.json b/flowtype/testsuite/typings.json new file mode 100644 index 00000000..373ac06c --- /dev/null +++ b/flowtype/testsuite/typings.json @@ -0,0 +1,6 @@ +{ + "ambientDependencies": { + "jasmine": "registry:dt/jasmine#2.2.0+20160317120654", + "node": "registry:dt/node#4.0.0+20160330064709" + } +} diff --git a/project/Build.scala b/project/Build.scala index bb180fdc..aa872cf9 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -172,18 +172,24 @@ object Courier extends Build with OverridablePublishSettings { .dependsOn(generatorApi) .disablePlugins(bintray.BintrayPlugin) + private[this] val flowtypeDir = file("flowtype") + lazy val flowtypeGenerator = Project(id = "flowtype-generator", base = flowtypeDir / "generator") + .dependsOn(generatorApi) + .disablePlugins(bintray.BintrayPlugin) + lazy val cli = Project(id = "courier-cli", base = file("cli")) .dependsOn( javaGenerator, androidGenerator, scalaGenerator, typescriptLiteGenerator, + flowtypeGenerator, swiftGenerator ).aggregate( javaGenerator, androidGenerator, scalaGenerator, - typescriptLiteGenerator, + flowtypeGenerator, swiftGenerator ).settings( executableFile := { @@ -205,6 +211,11 @@ object Courier extends Build with OverridablePublishSettings { .dependsOn(typescriptLiteGenerator) .disablePlugins(bintray.BintrayPlugin) + lazy val flowtypeGeneratorTest = Project( + id = "flowtype-generator-test", base = flowtypeDir / "generator-test") + .dependsOn(flowtypeGenerator) + .disablePlugins(bintray.BintrayPlugin) + lazy val courierSbtPlugin = Project(id = "sbt-plugin", base = file("sbt-plugin")) .dependsOn(scalaGenerator) .disablePlugins(xerial.sbt.Sonatype) @@ -237,6 +248,7 @@ object Courier extends Build with OverridablePublishSettings { s";project android-runtime;$publishCommand" + s";project swift-generator;$publishCommand" + s";project typescript-lite-generator;$publishCommand" + + s";project flowtype-generator;$publishCommand" + s";++$sbtScalaVersion;project scala-generator;$publishCommand" + s";++$currentScalaVersion;project scala-generator;$publishCommand" + s";++$sbtScalaVersion;project scala-runtime;$publishCommand" + @@ -266,6 +278,8 @@ object Courier extends Build with OverridablePublishSettings { swiftGenerator, typescriptLiteGenerator, typescriptLiteGeneratorTest, + flowtypeGenerator, + flowtypeGeneratorTest, cli) .settings(runtimeVersionSettings) .settings(packagedArtifacts := Map.empty) // disable publish for root aggregate module