diff --git a/.travis.yml b/.travis.yml index b35664a5..313069e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ language: scala scala: 2.10.5 jdk: oraclejdk8 +node_js: + - "4" + script: - sbt ++$TRAVIS_SCALA_VERSION fulltest diff --git a/project/Build.scala b/project/Build.scala index 601b120a..fbd4dd25 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -136,6 +136,14 @@ object Courier extends Build with OverridablePublishSettings { lazy val swiftGenerator = Project(id = "swift-generator", base = swiftDir / "generator") .dependsOn(generatorApi) + private[this] val typescriptLiteDir = file("typescript-lite") + lazy val typescriptLiteGenerator = Project(id = "typescript-lite-generator", base = typescriptLiteDir / "generator") + .dependsOn(generatorApi) + + lazy val typescriptLiteGeneratorTest = Project( + id = "typescript-lite-generator-test", base = typescriptLiteDir / "generator-test") + .dependsOn(typescriptLiteGenerator) + lazy val courierSbtPlugin = Project(id = "sbt-plugin", base = file("sbt-plugin")) .dependsOn(scalaGenerator) @@ -166,6 +174,7 @@ object Courier extends Build with OverridablePublishSettings { s";project android-generator;$publishCommand" + s";project android-runtime;$publishCommand" + s";project swift-generator;$publishCommand" + + s";project typescript-lite-generator;$publishCommand" + s";++$sbtScalaVersion;project scala-generator;$publishCommand" + s";++$currentScalaVersion;project scala-generator;$publishCommand" + s";++$sbtScalaVersion;project sbt-plugin;$publishCommand" + @@ -182,7 +191,9 @@ object Courier extends Build with OverridablePublishSettings { androidGenerator, androidGeneratorTest, androidRuntime, - swiftGenerator) + swiftGenerator, + typescriptLiteGenerator, + typescriptLiteGeneratorTest) .settings(runtimeVersionSettings) .settings(packagedArtifacts := Map.empty) // disable publish for root aggregate module .settings( diff --git a/reference-suite/src/main/courier/org/coursera/arrays/WithAnonymousUnionArray.courier b/reference-suite/src/main/courier/org/coursera/arrays/WithAnonymousUnionArray.courier new file mode 100644 index 00000000..49e8c632 --- /dev/null +++ b/reference-suite/src/main/courier/org/coursera/arrays/WithAnonymousUnionArray.courier @@ -0,0 +1,6 @@ +namespace org.coursera.arrays + +record WithAnonymousUnionArray { + unionsArray: array[union[int, string]] + unionsMap: map[string, union[string, int]] +} diff --git a/reference-suite/src/main/courier/org/coursera/enums/Fruits.courier b/reference-suite/src/main/courier/org/coursera/enums/Fruits.courier index 9e01db56..cfaa540d 100644 --- a/reference-suite/src/main/courier/org/coursera/enums/Fruits.courier +++ b/reference-suite/src/main/courier/org/coursera/enums/Fruits.courier @@ -1,5 +1,8 @@ namespace org.coursera.enums +/** + * An enum dedicated to the finest of the food groups. + */ enum Fruits { /** * An Apple. diff --git a/swift/generator/build.sbt b/swift/generator/build.sbt index 94f1eef7..23f28751 100644 --- a/swift/generator/build.sbt +++ b/swift/generator/build.sbt @@ -14,4 +14,3 @@ mainClass in assembly := Some("org.coursera.courier.SwiftGenerator") assemblyOption in assembly := (assemblyOption in assembly).value.copy(prependShellScript = Some(defaultShellScript)) assemblyJarName in assembly := s"${name.value}-${version.value}.jar" - diff --git a/typescript-lite/.gitignore b/typescript-lite/.gitignore new file mode 100644 index 00000000..79b9a21f --- /dev/null +++ b/typescript-lite/.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/typescript-lite/README.md b/typescript-lite/README.md new file mode 100644 index 00000000..ffa927b1 --- /dev/null +++ b/typescript-lite/README.md @@ -0,0 +1,312 @@ +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"; +} +``` + +```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$ +} +``` +**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/typescript-lite/generator-test/build.sbt b/typescript-lite/generator-test/build.sbt new file mode 100644 index 00000000..9f6f429e --- /dev/null +++ b/typescript-lite/generator-test/build.sbt @@ -0,0 +1,45 @@ +import sbt.inc.Analysis + +name := "courier-typescript-lite-generator-test" + +packagedArtifacts := Map.empty // do not publish + +libraryDependencies ++= Seq( + ExternalDependencies.JodaTime.jodaTime) + +autoScalaLibrary := false + +crossPaths := false + +// Test Generator +forkedVmCourierGeneratorSettings + +forkedVmCourierMainClass := "org.coursera.courier.TypeScriptLiteGenerator" + +forkedVmCourierClasspath := (dependencyClasspath in Runtime in typescriptLiteGenerator).value.files + +forkedVmSourceDirectory := (sourceDirectory in referenceSuite).value / "main" / "courier" + +forkedVmCourierDest := file("typescript-lite") / "testsuite" / "src" / "tslite-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 = """./typescript-lite/testsuite/full-build.sh"""! + + if (result != 0) { + throw new RuntimeException("NPM Build Failed") + } +} + +test in Test := (npmTest in Test).value diff --git a/typescript-lite/generator/.gitignore b/typescript-lite/generator/.gitignore new file mode 100644 index 00000000..19ec0db9 --- /dev/null +++ b/typescript-lite/generator/.gitignore @@ -0,0 +1,2 @@ +src/test/mainGeneratedPegasus +src/main/mainGeneratedPegasus diff --git a/typescript-lite/generator/build.sbt b/typescript-lite/generator/build.sbt new file mode 100644 index 00000000..aa4a5926 --- /dev/null +++ b/typescript-lite/generator/build.sbt @@ -0,0 +1,17 @@ +import sbtassembly.AssemblyPlugin.defaultShellScript + +name := "courier-typescript-lite-generator" + +plainJavaProjectSettings + +libraryDependencies ++= Seq( + ExternalDependencies.Rythm.rythmEngine, + ExternalDependencies.Slf4j.slf4jSimple) + +// Fat Jar +mainClass in assembly := Some("org.coursera.courier.TypeScriptLiteGenerator") + +assemblyOption in assembly := (assemblyOption in assembly).value.copy(prependShellScript = Some(defaultShellScript)) + +assemblyJarName in assembly := s"${name.value}-${version.value}.jar" + diff --git a/typescript-lite/generator/src/main/java/org/coursera/courier/TypeScriptLiteGenerator.java b/typescript-lite/generator/src/main/java/org/coursera/courier/TypeScriptLiteGenerator.java new file mode 100644 index 00000000..862d6ebe --- /dev/null +++ b/typescript-lite/generator/src/main/java/org/coursera/courier/TypeScriptLiteGenerator.java @@ -0,0 +1,171 @@ +/* + * 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; + +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.tslite.GlobalConfig; +import org.coursera.courier.tslite.TSProperties; +import org.coursera.courier.tslite.TSSyntax; +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 Typescript. + */ +public class TypeScriptLiteGenerator implements PegasusCodeGenerator { + private static final TSProperties.Optionality defaultOptionality = + TSProperties.Optionality.REQUIRED_FIELDS_MAY_BE_ABSENT; + + private final GlobalConfig globalConfig; + private final RythmEngine engine; + + public static void main(String[] args) throws Throwable { + // TODO(jbetz): use a CLI parser library + + 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(":"); + + TSProperties.Optionality optionality = defaultOptionality; + if (args.length > 3) { + optionality = TSProperties.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 TypeScriptLiteGenerator(globalConfig), options); + + for (File file: result.getTargetFiles()) { + System.out.println(file.getAbsolutePath()); + } + + InputStream runtime = ClassLoader.getSystemResourceAsStream("runtime/CourierRuntime.ts"); + IOUtils.copy(runtime, new FileOutputStream(new File(targetPath, "CourierRuntime.ts"))); + } + + public TypeScriptLiteGenerator() { + this(new GlobalConfig( + defaultOptionality, + false, + false)); + } + + public TypeScriptLiteGenerator(GlobalConfig globalConfig) { + this.globalConfig = globalConfig; + this.engine = new RythmEngine(); + this.engine.registerResourceLoader(new ClasspathResourceLoader(engine, "/")); + } + + public static class TSCompilationUnit extends GeneratedCodeTargetFile { + public TSCompilationUnit(String name, String namespace) { + super(name, namespace, "ts"); + } + } + + private static final PoorMansCStyleSourceFormatter formatter = + new PoorMansCStyleSourceFormatter(2, DocCommentStyle.ASTRISK_MARGIN); + + /** + * See {@link org.coursera.courier.tslite.TSProperties} for customization options. + */ + @Override + public GeneratedCode generate(ClassTemplateSpec templateSpec) { + + String code; + TSProperties TSProperties = globalConfig.lookupTSProperties(templateSpec); + if (TSProperties.omit) return null; + + TSSyntax syntax = new TSSyntax(TSProperties); + try { + if (templateSpec instanceof RecordTemplateSpec) { + code = engine.render("rythm-ts/record.txt", syntax.new TSRecordSyntax((RecordTemplateSpec) templateSpec)); + } else if (templateSpec instanceof EnumTemplateSpec) { + code = engine.render("rythm-ts/enum.txt", syntax.new TSEnumSyntax((EnumTemplateSpec) templateSpec)); + } else if (templateSpec instanceof UnionTemplateSpec) { + code = engine.render("rythm-ts/union.txt", syntax.new TSUnionSyntax((UnionTemplateSpec) templateSpec)); + } else if (templateSpec instanceof TyperefTemplateSpec) { + TyperefTemplateSpec typerefSpec = (TyperefTemplateSpec) templateSpec; + code = engine.render("rythm-ts/typeref.txt", syntax.TSTyperefSyntaxCreate(typerefSpec)); + } else if (templateSpec instanceof FixedTemplateSpec) { + code = engine.render("rythm-ts/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); + } + TSCompilationUnit compilationUnit = + new TSCompilationUnit( + 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/typescript-lite/generator/src/main/java/org/coursera/courier/tslite/GlobalConfig.java b/typescript-lite/generator/src/main/java/org/coursera/courier/tslite/GlobalConfig.java new file mode 100644 index 00000000..8ec7c362 --- /dev/null +++ b/typescript-lite/generator/src/main/java/org/coursera/courier/tslite/GlobalConfig.java @@ -0,0 +1,47 @@ +package org.coursera.courier.tslite; + +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 TSProperties defaults; + + public GlobalConfig( + TSProperties.Optionality defaultOptionality, + boolean defaultEquatable, + boolean defaultOmit) { + defaults = new TSProperties(defaultOptionality, defaultEquatable, defaultOmit); + } + + public TSProperties lookupTSProperties(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"); + + TSProperties.Optionality optionality = + optionalityString == null ? defaults.optionality : TSProperties.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 TSProperties(optionality, equatable, omit); + } + } +} diff --git a/typescript-lite/generator/src/main/java/org/coursera/courier/tslite/TSProperties.java b/typescript-lite/generator/src/main/java/org/coursera/courier/tslite/TSProperties.java new file mode 100644 index 00000000..69d0766e --- /dev/null +++ b/typescript-lite/generator/src/main/java/org/coursera/courier/tslite/TSProperties.java @@ -0,0 +1,67 @@ +package org.coursera.courier.tslite; + +/** + * 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 TSProperties { + + /** + * "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 TSProperties(Optionality optionality, boolean equatable, boolean omit) { + this.optionality = optionality; + this.equatable = equatable; + this.omit = omit; + } + +} diff --git a/typescript-lite/generator/src/main/java/org/coursera/courier/tslite/TSSyntax.java b/typescript-lite/generator/src/main/java/org/coursera/courier/tslite/TSSyntax.java new file mode 100644 index 00000000..f942e8e7 --- /dev/null +++ b/typescript-lite/generator/src/main/java/org/coursera/courier/tslite/TSSyntax.java @@ -0,0 +1,1031 @@ +/* + * 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.tslite; + +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.tslite.TSProperties.Optionality; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 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 TSEnumSyntax}, {@link TSUnionSyntax}, {@link TSRecordSyntax}, and {@link TSTyperefSyntax} are + * used directly to populate the templates. + * + * @see TSPrimitiveTypeSyntax + * @see TSEnumSyntax + * @see TSArraySyntax + * @see TSUnionSyntax + * @see TSMapSyntax + * @see TSTyperefSyntax + * @see TSRecordSyntax + * @see TSFixedSyntax + * @see TSRecordSyntax + * @see TSUnionSyntax + */ +public class TSSyntax { + + /** Config properties passed from the command line parser */ + private final TSProperties TSProperties; + + public TSSyntax(TSProperties TSProperties) { + this.TSProperties = TSProperties; + } + + /** + * 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 an underscore 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 { ") + .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 TSTypeSyntax { + + /** 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 TSSyntax} class-level docs for more info) + */ + private TSTypeSyntax createTypeSyntax(ClassTemplateSpec template) { + if (template instanceof RecordTemplateSpec) { + return new TSRecordSyntax((RecordTemplateSpec) template); + } else if (template instanceof TyperefTemplateSpec) { + return TSTyperefSyntaxCreate((TyperefTemplateSpec) template); + } else if (template instanceof FixedTemplateSpec) { + return TSFixedSyntaxCreate((FixedTemplateSpec) template); + } else if (template instanceof EnumTemplateSpec) { + return new TSEnumSyntax((EnumTemplateSpec) template); + } else if (template instanceof PrimitiveTemplateSpec) { + return new TSPrimitiveTypeSyntax((PrimitiveTemplateSpec) template); + } else if (template instanceof MapTemplateSpec) { + return new TSMapSyntax((MapTemplateSpec) template); + } else if (template instanceof ArrayTemplateSpec) { + return new TSArraySyntax((ArrayTemplateSpec) template); + } else if (template instanceof UnionTemplateSpec) { + return new TSUnionSyntax((UnionTemplateSpec) template); + } else { + throw new RuntimeException("Unrecognized template spec: " + template + " with schema " + template.getSchema()); + } + } + + /** Convenience wrapper around {@link #createTypeSyntax(ClassTemplateSpec)}. */ + private TSTypeSyntax 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(TSTypeSyntax syntax) { + if (syntax instanceof TSEnclosedTypeSyntax) { + return ((TSEnclosedTypeSyntax) syntax).typeNameQualifiedByEnclosedType(); + } else { + return syntax.typeName(); + } + } + + /** TS-specific syntax for Maps */ + private class TSMapSyntax implements TSTypeSyntax { + private final MapTemplateSpec _template; + + TSMapSyntax(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: TSRecordFieldSyntax. + // 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; + TSTypeSyntax 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 { 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 TSMapSyntax members + // + private TSTypeSyntax _valueTypeSyntax() { + return createTypeSyntax(_template.getSchema().getValues()); + } + } + + /** TS-specific syntax for Arrays */ + private class TSArraySyntax implements TSTypeSyntax { + 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: TSRecordFieldSyntax. + // 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; + TSTypeSyntax 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 TSTypeSyntax _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 TSTypeSyntax { + 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 TSNamedTypeSyntax { + private final NamedDataSchema _dataSchema; + + public TSNamedTypeSyntax(NamedDataSchema _dataSchema) { + this._dataSchema = _dataSchema; + } + + public String typeName() { + return TSSyntax.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 TSTypeSyntax { + private final FixedTemplateSpec _template; + private final TSNamedTypeSyntax _namedSyntax; + + public TSFixedSyntax(FixedTemplateSpec template, TSNamedTypeSyntax 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 TSNamedTypeSyntax(template.getSchema())); + } + + /** + * TS representation of a Union type's member (e.g. the "int" in "union[int]"). + */ + public class TSUnionMemberSyntax { + private final TSUnionSyntax _parentSyntax; + private final UnionDataSchema _schema; + private final UnionTemplateSpec.Member _member; + + public TSUnionMemberSyntax(TSUnionSyntax _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 TSTypeSyntax _memberTypeSyntax() { + return createTypeSyntax(_member.getSchema()); + } + } + + /** TS-specific representation of a Union type. */ + public class TSUnionSyntax implements TSTypeSyntax, TSEnclosedTypeSyntax { + private final UnionTemplateSpec _template; + private final UnionDataSchema _schema; + + public TSUnionSyntax(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 + TSTyperefSyntax refSyntax = TSTyperefSyntaxCreate(_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 (TSUnionMemberSyntax 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 TSNamedTypeSyntax(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); + TSUnionMemberSyntax 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); + TSUnionMemberSyntax member = members.get(i); + sb.append(member.fullUnionMemberTypeName()); + + 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 TSUnionMemberSyntax(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 TS representation of a single field in a Record */ + public class TSRecordFieldSyntax implements TSTypeSyntax { + private final RecordTemplateSpec _template; + private final RecordDataSchema _schema; + private final RecordTemplateSpec.Field _field; + + public TSRecordFieldSyntax(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. + TSTypeSyntax 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 || TSProperties.optionality == Optionality.REQUIRED_FIELDS_MAY_BE_ABSENT; + + return markFieldAsOptional? "?": ""; + } + + // + // Private members + // + private RecordDataSchema.Field _schemaField() { + return _field.getSchemaField(); + } + private TSTypeSyntax _fieldTypeSyntax() { + return createTypeSyntax(_schemaField().getType()); + } + } + + /** TS-specific syntax for Records */ + public class TSRecordSyntax implements TSTypeSyntax { + private final RecordTemplateSpec _template; + private final RecordDataSchema _schema; + private final TSNamedTypeSyntax _namedTypeSyntax; + + public TSRecordSyntax(RecordTemplateSpec _template) { + this._template = _template; + this._schema = _template.getSchema(); + this._namedTypeSyntax = new TSNamedTypeSyntax(_schema); + } + + public String docString() { + return _namedTypeSyntax.docString(); + } + + public List fields() { + List fields = new ArrayList<>(); + + for (RecordTemplateSpec.Field fieldSpec: _template.getFields()) { + fields.add(new TSRecordFieldSyntax(_template, fieldSpec)); + } + + return fields; + } + + public Set enclosedUnions() { + Set unions = new HashSet<>(); + for (ClassTemplateSpec spec: ClassTemplateSpecs.allContainedTypes(_template)) { + if (spec instanceof UnionTemplateSpec) { + unions.add(new TSUnionSyntax((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 typescript import block for this record */ + public String imports() { + Set imports = new HashSet<>(); + + for (TSRecordFieldSyntax fieldSyntax: this.fields()) { + imports.addAll(fieldSyntax.typeModules()); + } + + for (TSUnionSyntax union: this.enclosedUnions()) { + for (TSUnionMemberSyntax unionMember: union.members()) { + imports.addAll(unionMember.typeModules()); + } + } + + return flattenImports(imports); + } + } + + /** TS syntax for typerefs. */ + public class TSTyperefSyntax implements TSTypeSyntax { + private final TyperefTemplateSpec _template; + private final TyperefDataSchema _dataSchema; + private final TSNamedTypeSyntax _namedTypeSyntax; + + public TSTyperefSyntax(TyperefTemplateSpec _template, TyperefDataSchema _dataSchema, TSNamedTypeSyntax _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 TSTyperefSyntax TSTyperefSyntaxCreate(TyperefTemplateSpec template) { + return new TSTyperefSyntax(template, template.getSchema(), new TSNamedTypeSyntax(template.getSchema())); + } + + /** TS syntax for the symbol of an enum */ + public class TSEnumSymbolSyntax { + private final EnumTemplateSpec _template; + private final EnumDataSchema _dataSchema; + private final String _symbolString; + + public TSEnumSymbolSyntax(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() { + DataMap docsBySymbol = (DataMap) _dataSchema.getProperties().getOrDefault("symbolDocs", new DataMap()); + String symbolDoc = docsBySymbol.getString(_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 TSEnumSymbolSyntax}. */ + public class TSEnumSyntax implements TSTypeSyntax { + private final EnumTemplateSpec _template; + private final EnumDataSchema _dataSchema; + private final TSNamedTypeSyntax _namedTypeSyntax; + + public TSEnumSyntax(EnumTemplateSpec _template) { + this._template = _template; + this._dataSchema = _template.getSchema(); + this._namedTypeSyntax = new TSNamedTypeSyntax(_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 + * + * export type Fruit = "APPLE" | "ORANGE"; + **/ + public String stringLiteralUnion() { + List symbols = this.symbols(); + if (this.symbols().size() == 0) { + return "void"; // Helps us compile if some bozo declared an empty union. + } else { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < symbols.size(); i++) { + TSEnumSymbolSyntax symbol = symbols.get(i); + boolean isLast = (i + 1 == symbols.size()); + sb.append(symbol.stringLiteralValue()); + + if (!isLast) { + sb.append(" | "); + } + } + return sb.toString(); + } + } + + @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 TSEnumSymbolSyntax(_template, _dataSchema, symbol)); + } + return symbols; + } + } +} diff --git a/typescript-lite/generator/src/main/resources/runtime/CourierRuntime.ts b/typescript-lite/generator/src/main/resources/runtime/CourierRuntime.ts new file mode 100644 index 00000000..6272d9be --- /dev/null +++ b/typescript-lite/generator/src/main/resources/runtime/CourierRuntime.ts @@ -0,0 +1,20 @@ +// +// 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/typescript-lite/generator/src/main/resources/rythm-ts/enum.txt b/typescript-lite/generator/src/main/resources/rythm-ts/enum.txt new file mode 100644 index 00000000..1dee4f17 --- /dev/null +++ b/typescript-lite/generator/src/main/resources/rythm-ts/enum.txt @@ -0,0 +1,13 @@ +@args org.coursera.courier.tslite.TSSyntax.TSEnumSyntax enumeration +@import org.coursera.courier.tslite.TSSyntax.TSEnumSymbolSyntax + +@enumeration.docString() +export type @enumeration.typeName() = @enumeration.stringLiteralUnion(); +@if(enumeration.requiresCompanionModule()) { + export module @enumeration.typeName() { + @for(TSEnumSymbolSyntax symbol : enumeration.symbols()) { + @symbol.docString() + export const @symbol.moduleConstValue(): @enumeration.typeName() = @symbol.stringLiteralValue(); + } + } +} diff --git a/typescript-lite/generator/src/main/resources/rythm-ts/fixed.txt b/typescript-lite/generator/src/main/resources/rythm-ts/fixed.txt new file mode 100644 index 00000000..3a151c18 --- /dev/null +++ b/typescript-lite/generator/src/main/resources/rythm-ts/fixed.txt @@ -0,0 +1,4 @@ +@args org.coursera.courier.tslite.TSSyntax.TSFixedSyntax fixed + +@fixed.docString() +export type @fixed.typeName() = string; diff --git a/typescript-lite/generator/src/main/resources/rythm-ts/record.txt b/typescript-lite/generator/src/main/resources/rythm-ts/record.txt new file mode 100644 index 00000000..3a39d2a5 --- /dev/null +++ b/typescript-lite/generator/src/main/resources/rythm-ts/record.txt @@ -0,0 +1,21 @@ +@args org.coursera.courier.tslite.TSSyntax.TSRecordSyntax record +@import org.coursera.courier.tslite.TSSyntax.TSUnionSyntax +@import org.coursera.courier.tslite.TSSyntax.TSRecordFieldSyntax + +@record.imports() + +@record.docString() +export interface @record.typeName() { + @for(TSRecordFieldSyntax field: record.fields()) { + @field.docString() + @field.accessorName() @field.questionMarkIfOptional(): @field.typeName(); + } +} + +@if(record.requiresCompanionModule()) { + export module @record.typeName() { + @for(TSUnionSyntax union: record.enclosedUnions()) { + @union(union) + } + } +} diff --git a/typescript-lite/generator/src/main/resources/rythm-ts/typeref.txt b/typescript-lite/generator/src/main/resources/rythm-ts/typeref.txt new file mode 100644 index 00000000..4b5a2ef4 --- /dev/null +++ b/typescript-lite/generator/src/main/resources/rythm-ts/typeref.txt @@ -0,0 +1,6 @@ +@args org.coursera.courier.tslite.TSSyntax.TSTyperefSyntax typeref + +@typeref.imports() + +@typeref.docString() +export type @typeref.typeName() = @typeref.refTypeName(); diff --git a/typescript-lite/generator/src/main/resources/rythm-ts/union.txt b/typescript-lite/generator/src/main/resources/rythm-ts/union.txt new file mode 100644 index 00000000..31b3308b --- /dev/null +++ b/typescript-lite/generator/src/main/resources/rythm-ts/union.txt @@ -0,0 +1,28 @@ +@args org.coursera.courier.tslite.TSSyntax.TSUnionSyntax union +@import org.coursera.courier.tslite.TSSyntax.TSUnionSyntax +@import org.coursera.courier.tslite.TSSyntax.TSUnionMemberSyntax + +@union.imports() + +@union.docString() +export type @union.typeName() = @union.memberUnionTypeExpression(); +@if(union.requiresCompanionModule()) { + export module @union.typeName() { + export interface @union.memberBaseTypeName() { + [key: string]: @union.unionTypeExpression(); + } + + @for(TSUnionMemberSyntax member: union.members()) { + export interface @member.unionMemberTypeName() extends @union.memberBaseTypeName() { + "@member.unionMemberKey()": @member.typeName(); + } + } + export function unpack(union: @union.typeName()) { + return { + @for(TSUnionMemberSyntax member: union.members()) { + @member.unpackString(): union["@member.unionMemberKey()"] as @(member.typeName())@if(!member_isLast){,} + } + }; + } + } +} diff --git a/typescript-lite/testsuite/.gitignore b/typescript-lite/testsuite/.gitignore new file mode 100644 index 00000000..ce38a3ce --- /dev/null +++ b/typescript-lite/testsuite/.gitignore @@ -0,0 +1,5 @@ +node_modules +.tmp +typings +src/tslite-bindings +npm-debug.log diff --git a/typescript-lite/testsuite/full-build.sh b/typescript-lite/testsuite/full-build.sh new file mode 100755 index 00000000..c15fd89c --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/package.json b/typescript-lite/testsuite/package.json new file mode 100644 index 00000000..107ab579 --- /dev/null +++ b/typescript-lite/testsuite/package.json @@ -0,0 +1,28 @@ +{ + "name": "courier-typescript-lite-generator-test", + "version": "1.0.0", + "description": "Test-suite for the typescript-lite courier bindings", + "main": "src/index.js", + "scripts": { + "setup": "npm install && npm run-script typings-install", + "compile": "./node_modules/.bin/tsc", + "typings-install": "./node_modules/.bin/typings install", + "test": "npm run-script compile && ./node_modules/.bin/jasmine", + "full-build": "npm run-script setup && npm run-script test" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/coursera/courier.git" + }, + "author": "Erem Boto", + "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" + } +} diff --git a/typescript-lite/testsuite/spec/support/jasmine.json b/typescript-lite/testsuite/spec/support/jasmine.json new file mode 100644 index 00000000..901fcf15 --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/src/compilation-failures/array_bad-item-type-enum-expected.ts b/typescript-lite/testsuite/src/compilation-failures/array_bad-item-type-enum-expected.ts new file mode 100644 index 00000000..bfb65315 --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/src/compilation-failures/array_bad-item-type.ts b/typescript-lite/testsuite/src/compilation-failures/array_bad-item-type.ts new file mode 100644 index 00000000..d45c8fb5 --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/src/compilation-failures/enum_bad-string.ts b/typescript-lite/testsuite/src/compilation-failures/enum_bad-string.ts new file mode 100644 index 00000000..a605d1f0 --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/src/compilation-failures/map_bad-value-type.ts b/typescript-lite/testsuite/src/compilation-failures/map_bad-value-type.ts new file mode 100644 index 00000000..39703fb8 --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/src/compilation-failures/record_wrong-field-type.ts b/typescript-lite/testsuite/src/compilation-failures/record_wrong-field-type.ts new file mode 100644 index 00000000..7d59088f --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/src/compilation-failures/tslite-bindings b/typescript-lite/testsuite/src/compilation-failures/tslite-bindings new file mode 120000 index 00000000..b9513be7 --- /dev/null +++ b/typescript-lite/testsuite/src/compilation-failures/tslite-bindings @@ -0,0 +1 @@ +../tslite-bindings \ No newline at end of file diff --git a/typescript-lite/testsuite/src/compilation-failures/typeref_wrong-type.ts b/typescript-lite/testsuite/src/compilation-failures/typeref_wrong-type.ts new file mode 100644 index 00000000..abee8c97 --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/src/compilation-failures/union_bad-body-content.ts b/typescript-lite/testsuite/src/compilation-failures/union_bad-body-content.ts new file mode 100644 index 00000000..b285e93b --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/src/compilation-failures/union_bad-lookup-string.ts b/typescript-lite/testsuite/src/compilation-failures/union_bad-lookup-string.ts new file mode 100644 index 00000000..36e0ece8 --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/src/expected-successes/spec/bindings.spec.ts b/typescript-lite/testsuite/src/expected-successes/spec/bindings.spec.ts new file mode 100644 index 00000000..281ccc1b --- /dev/null +++ b/typescript-lite/testsuite/src/expected-successes/spec/bindings.spec.ts @@ -0,0 +1,635 @@ +import { WithoutNamespace } from "../tslite-bindings/.WithoutNamespace"; +import { Map } from "../tslite-bindings/CourierRuntime"; +import { WithCustomArrayTestId } from "../tslite-bindings/org.coursera.arrays.WithCustomArrayTestId"; +import { WithCustomTypesArray } from "../tslite-bindings/org.coursera.arrays.WithCustomTypesArray"; +import { WithCustomTypesArrayUnion } from "../tslite-bindings/org.coursera.arrays.WithCustomTypesArrayUnion"; +import { WithPrimitivesArray } from "../tslite-bindings/org.coursera.arrays.WithPrimitivesArray"; +import { WithRecordArray } from "../tslite-bindings/org.coursera.arrays.WithRecordArray"; +import { BooleanId } from "../tslite-bindings/org.coursera.customtypes.BooleanId"; +import { BoxedIntId } from "../tslite-bindings/org.coursera.customtypes.BoxedIntId"; +import { ByteId } from "../tslite-bindings/org.coursera.customtypes.ByteId"; +import { CaseClassCustomIntWrapper } from "../tslite-bindings/org.coursera.customtypes.CaseClassCustomIntWrapper"; +import { CaseClassStringIdWrapper } from "../tslite-bindings/org.coursera.customtypes.CaseClassStringIdWrapper"; +import { CharId } from "../tslite-bindings/org.coursera.customtypes.CharId"; +import { CustomArrayTestId } from "../tslite-bindings/org.coursera.customtypes.CustomArrayTestId"; +import { CustomInt } from "../tslite-bindings/org.coursera.customtypes.CustomInt"; +import { CustomIntWrapper } from "../tslite-bindings/org.coursera.customtypes.CustomIntWrapper"; +import { CustomMapTestKeyId } from "../tslite-bindings/org.coursera.customtypes.CustomMapTestKeyId"; +import { CustomMapTestValueId } from "../tslite-bindings/org.coursera.customtypes.CustomMapTestValueId"; +import { CustomRecord } from "../tslite-bindings/org.coursera.customtypes.CustomRecord"; +import { CustomRecordTestId } from "../tslite-bindings/org.coursera.customtypes.CustomRecordTestId"; +import { CustomUnionTestId } from "../tslite-bindings/org.coursera.customtypes.CustomUnionTestId"; +import { DateTime } from "../tslite-bindings/org.coursera.customtypes.DateTime"; +import { DoubleId } from "../tslite-bindings/org.coursera.customtypes.DoubleId"; +import { FloatId } from "../tslite-bindings/org.coursera.customtypes.FloatId"; +import { IntId } from "../tslite-bindings/org.coursera.customtypes.IntId"; +import { LongId } from "../tslite-bindings/org.coursera.customtypes.LongId"; +import { ShortId } from "../tslite-bindings/org.coursera.customtypes.ShortId"; +import { StringId } from "../tslite-bindings/org.coursera.customtypes.StringId"; +import { DeprecatedRecord } from "../tslite-bindings/org.coursera.deprecated.DeprecatedRecord"; +import { EmptyEnum } from "../tslite-bindings/org.coursera.enums.EmptyEnum"; +import { EnumProperties } from "../tslite-bindings/org.coursera.enums.EnumProperties"; +import { Fruits } from "../tslite-bindings/org.coursera.enums.Fruits"; +import { DefaultLiteralEscaping } from "../tslite-bindings/org.coursera.escaping.DefaultLiteralEscaping"; +import { KeywordEscaping } from "../tslite-bindings/org.coursera.escaping.KeywordEscaping"; +import { ReservedClassFieldEscaping } from "../tslite-bindings/org.coursera.escaping.ReservedClassFieldEscaping"; +import { class$ } from "../tslite-bindings/org.coursera.escaping.class"; +import { WithFixed8 } from "../tslite-bindings/org.coursera.fixed.WithFixed8"; +import { Toggle } from "../tslite-bindings/org.coursera.maps.Toggle"; +import { WithComplexTypesMap } from "../tslite-bindings/org.coursera.maps.WithComplexTypesMap"; +import { WithComplexTypesMapUnion } from "../tslite-bindings/org.coursera.maps.WithComplexTypesMapUnion"; +import { WithCustomMapTestIds } from "../tslite-bindings/org.coursera.maps.WithCustomMapTestIds"; +import { WithCustomTypesMap } from "../tslite-bindings/org.coursera.maps.WithCustomTypesMap"; +import { WithPrimitivesMap } from "../tslite-bindings/org.coursera.maps.WithPrimitivesMap"; +import { WithTypedKeyMap } from "../tslite-bindings/org.coursera.maps.WithTypedKeyMap"; +import { CourierFile } from "../tslite-bindings/org.coursera.records.CourierFile"; +import { JsonTest } from "../tslite-bindings/org.coursera.records.JsonTest"; +import { Message } from "../tslite-bindings/org.coursera.records.Message"; +import { WithAnonymousUnionArray } from "../tslite-bindings/org.coursera.arrays.WithAnonymousUnionArray"; +import { Note } from "../tslite-bindings/org.coursera.records.Note"; +import { WithDateTime } from "../tslite-bindings/org.coursera.records.WithDateTime"; +import { WithFlatTypedDefinition } from "../tslite-bindings/org.coursera.records.WithFlatTypedDefinition"; +import { WithInclude } from "../tslite-bindings/org.coursera.records.WithInclude"; +import { WithTypedDefinition } from "../tslite-bindings/org.coursera.records.WithTypedDefinition"; +import { WithUnion } from "../tslite-bindings/org.coursera.records.WithUnion"; +import { Fixed8 } from "../tslite-bindings/org.coursera.fixed.Fixed8"; +import { class$ as EscapedClassRecord} from "../tslite-bindings/org.coursera.records.class"; +import { Simple } from "../tslite-bindings/org.coursera.records.primitivestyle.Simple"; +import { WithComplexTypes } from "../tslite-bindings/org.coursera.records.primitivestyle.WithComplexTypes"; +import { WithPrimitives } from "../tslite-bindings/org.coursera.records.primitivestyle.WithPrimitives"; +import { BooleanTyperef } from "../tslite-bindings/org.coursera.records.test.BooleanTyperef"; +import { BytesTyperef } from "../tslite-bindings/org.coursera.records.test.BytesTyperef"; +import { DoubleTyperef } from "../tslite-bindings/org.coursera.records.test.DoubleTyperef"; +import { Empty } from "../tslite-bindings/org.coursera.records.test.Empty"; +import { FloatTyperef } from "../tslite-bindings/org.coursera.records.test.FloatTyperef"; +import { InlineOptionalRecord } from "../tslite-bindings/org.coursera.records.test.InlineOptionalRecord"; +import { InlineRecord } from "../tslite-bindings/org.coursera.records.test.InlineRecord"; +import { IntCustomType as TestIntCustomType } from "../tslite-bindings/org.coursera.records.test.IntCustomType"; +import { IntTyperef as TestIntTyperef } from "../tslite-bindings/org.coursera.records.test.IntTyperef"; +import { LongTyperef } from "../tslite-bindings/org.coursera.records.test.LongTyperef"; +import { Message as TestMessage } from "../tslite-bindings/org.coursera.records.test.Message"; +import { NumericDefaults } from "../tslite-bindings/org.coursera.records.test.NumericDefaults"; +import { OptionalBooleanTyperef } from "../tslite-bindings/org.coursera.records.test.OptionalBooleanTyperef"; +import { OptionalBytesTyperef } from "../tslite-bindings/org.coursera.records.test.OptionalBytesTyperef"; +import { OptionalDoubleTyperef } from "../tslite-bindings/org.coursera.records.test.OptionalDoubleTyperef"; +import { OptionalFloatTyperef } from "../tslite-bindings/org.coursera.records.test.OptionalFloatTyperef"; +import { OptionalIntCustomType } from "../tslite-bindings/org.coursera.records.test.OptionalIntCustomType"; +import { OptionalIntTyperef } from "../tslite-bindings/org.coursera.records.test.OptionalIntTyperef"; +import { OptionalLongTyperef } from "../tslite-bindings/org.coursera.records.test.OptionalLongTyperef"; +import { OptionalStringTyperef } from "../tslite-bindings/org.coursera.records.test.OptionalStringTyperef"; +import { RecursivelyDefinedRecord } from "../tslite-bindings/org.coursera.records.test.RecursivelyDefinedRecord"; +import { Simple as TestSimple } from "../tslite-bindings/org.coursera.records.test.Simple"; +import { StringTyperef } from "../tslite-bindings/org.coursera.records.test.StringTyperef"; +import { With22Fields } from "../tslite-bindings/org.coursera.records.test.With22Fields"; +import { With23Fields } from "../tslite-bindings/org.coursera.records.test.With23Fields"; +import { WithCaseClassCustomType } from "../tslite-bindings/org.coursera.records.test.WithCaseClassCustomType"; +import { WithComplexTypeDefaults } from "../tslite-bindings/org.coursera.records.test.WithComplexTypeDefaults"; +import { WithComplexTyperefs } from "../tslite-bindings/org.coursera.records.test.WithComplexTyperefs"; +import { WithComplexTypes as TestWithComplexTypes } from "../tslite-bindings/org.coursera.records.test.WithComplexTypes"; +import { WithCourierFile } from "../tslite-bindings/org.coursera.records.test.WithCourierFile"; +import { WithCustomIntWrapper } from "../tslite-bindings/org.coursera.records.test.WithCustomIntWrapper"; +import { WithCustomRecord } from "../tslite-bindings/org.coursera.records.test.WithCustomRecord"; +import { WithCustomRecordTestId } from "../tslite-bindings/org.coursera.records.test.WithCustomRecordTestId"; +import { WithDateTime as TestWithDateTime } from "../tslite-bindings/org.coursera.records.test.WithDateTime"; +import { WithInclude as TestWithInclude } from "../tslite-bindings/org.coursera.records.test.WithInclude"; +import { WithInlineRecord } from "../tslite-bindings/org.coursera.records.test.WithInlineRecord"; +import { WithOmitField } from "../tslite-bindings/org.coursera.records.test.WithOmitField"; +import { WithOptionalComplexTypeDefaults } from "../tslite-bindings/org.coursera.records.test.WithOptionalComplexTypeDefaults"; +import { WithOptionalComplexTypes } from "../tslite-bindings/org.coursera.records.test.WithOptionalComplexTypes"; +import { WithOptionalComplexTypesDefaultNone } from "../tslite-bindings/org.coursera.records.test.WithOptionalComplexTypesDefaultNone"; +import { WithOptionalPrimitiveCustomTypes } from "../tslite-bindings/org.coursera.records.test.WithOptionalPrimitiveCustomTypes"; +import { WithOptionalPrimitiveDefaultNone } from "../tslite-bindings/org.coursera.records.test.WithOptionalPrimitiveDefaultNone"; +import { WithOptionalPrimitiveDefaults } from "../tslite-bindings/org.coursera.records.test.WithOptionalPrimitiveDefaults"; +import { WithOptionalPrimitiveTyperefs } from "../tslite-bindings/org.coursera.records.test.WithOptionalPrimitiveTyperefs"; +import { WithOptionalPrimitives } from "../tslite-bindings/org.coursera.records.test.WithOptionalPrimitives"; +import { WithPrimitiveCustomTypes } from "../tslite-bindings/org.coursera.records.test.WithPrimitiveCustomTypes"; +import { WithPrimitiveDefaults } from "../tslite-bindings/org.coursera.records.test.WithPrimitiveDefaults"; +import { WithPrimitiveTyperefs } from "../tslite-bindings/org.coursera.records.test.WithPrimitiveTyperefs"; +import { WithPrimitives as TestWithPrimitives } from "../tslite-bindings/org.coursera.records.test.WithPrimitives"; +import { WithUnionWithInlineRecord } from "../tslite-bindings/org.coursera.records.test.WithUnionWithInlineRecord"; +import { ArrayTyperef } from "../tslite-bindings/org.coursera.typerefs.ArrayTyperef"; +import { EnumTyperef } from "../tslite-bindings/org.coursera.typerefs.EnumTyperef"; +import { FlatTypedDefinition } from "../tslite-bindings/org.coursera.typerefs.FlatTypedDefinition"; +import { InlineRecord as InlineRecordTypeRef } from "../tslite-bindings/org.coursera.typerefs.InlineRecord"; +import { InlineRecord2 } from "../tslite-bindings/org.coursera.typerefs.InlineRecord2"; +import { IntTyperef } from "../tslite-bindings/org.coursera.typerefs.IntTyperef"; +import { MapTyperef } from "../tslite-bindings/org.coursera.typerefs.MapTyperef"; +import { RecordTyperef } from "../tslite-bindings/org.coursera.typerefs.RecordTyperef"; +import { TypedDefinition } from "../tslite-bindings/org.coursera.typerefs.TypedDefinition"; +import { Union } from "../tslite-bindings/org.coursera.typerefs.Union"; +import { UnionTyperef } from "../tslite-bindings/org.coursera.typerefs.UnionTyperef"; +import { UnionWithInlineRecord } from "../tslite-bindings/org.coursera.typerefs.UnionWithInlineRecord"; +import { IntCustomType } from "../tslite-bindings/org.coursera.unions.IntCustomType"; +import { IntTyperef as IntTyperefUnion} from "../tslite-bindings/org.coursera.unions.IntTyperef"; +import { WithComplexTypesUnion } from "../tslite-bindings/org.coursera.unions.WithComplexTypesUnion"; +import { WithCustomUnionTestId } from "../tslite-bindings/org.coursera.unions.WithCustomUnionTestId"; +import { WithEmptyUnion } from "../tslite-bindings/org.coursera.unions.WithEmptyUnion"; +import { WithPrimitiveCustomTypesUnion } from "../tslite-bindings/org.coursera.unions.WithPrimitiveCustomTypesUnion"; +import { WithPrimitiveTyperefsUnion } from "../tslite-bindings/org.coursera.unions.WithPrimitiveTyperefsUnion"; +import { WithRecordCustomTypeUnion } from "../tslite-bindings/org.coursera.unions.WithRecordCustomTypeUnion"; +import { Fortune } from "../tslite-bindings/org.example.Fortune"; +import { FortuneCookie } from "../tslite-bindings/org.example.FortuneCookie"; +import { FortuneTelling } from "../tslite-bindings/org.example.FortuneTelling"; +import { MagicEightBall } from "../tslite-bindings/org.example.MagicEightBall"; +import { MagicEightBallAnswer } from "../tslite-bindings/org.example.MagicEightBallAnswer"; +import { TyperefExample } from "../tslite-bindings/org.example.TyperefExample"; +import { DateTime as CommonDateTime } from "../tslite-bindings/org.example.common.DateTime"; +import { Timestamp } from "../tslite-bindings/org.example.common.Timestamp"; +import { DateTime as OtherDateTime } from "../tslite-bindings/org.example.other.DateTime"; +import { record } from "../tslite-bindings/org.example.record"; +import { WithPrimitivesUnion } from "../tslite-bindings/org.coursera.unions.WithPrimitivesUnion"; +import * as ts from "typescript"; + +import CustomMatcherFactories = jasmine.CustomMatcherFactories; +import CompilerOptions = ts.CompilerOptions; +import TranspileOptions = ts.TranspileOptions; +import Diagnostic = ts.Diagnostic; + +// 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); + }); + }); +}); + +// +// Now attempt to compile our examples from src/compilation-failures. They should all fail. +// +describe("The typescript compiler", () => { + beforeEach(() => { + jasmine.addMatchers(toCompileMatcher); + }); + + const expectations = [ + ["should not allow unions with incorrect lookup keys", "union_bad-lookup-string.ts"], + ["should not allow the body of the union to be malformed", "union_bad-body-content.ts"], + ["should not allow records to have the wrong field type", "record_wrong-field-type.ts"], + ["should not allow enums with a bad string value", "enum_bad-string.ts"], + ["should not allow typerefs with the wrong root type", "typeref_wrong-type.ts"], + ["should not allow the wrong type as the item of a primitive array", "array_bad-item-type.ts"], + ["should not allow the wrong type as the item of an enum array", "array_bad-item-type-enum-expected.ts"], + ["should not allow the wrong type as the value of a map", "map_bad-value-type.ts"] + ]; + + expectations.forEach((expectationPair) => { + const [testCaseName, fileTest] = expectationPair; + it(testCaseName, () => { + expect(`src/compilation-failures/${fileTest}`).not.toCompile("It was expected to fail."); + }) + }); +}); + +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"); + + switch (fruit1) { + case Fruits.APPLE: + result = "It's still an apple"; + break; + default: + result = "Something else." + } + + expect(result).toEqual("It's still an apple"); + }); +}); + + +// +// 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/typescript-lite/testsuite/src/expected-successes/tslite-bindings b/typescript-lite/testsuite/src/expected-successes/tslite-bindings new file mode 120000 index 00000000..b9513be7 --- /dev/null +++ b/typescript-lite/testsuite/src/expected-successes/tslite-bindings @@ -0,0 +1 @@ +../tslite-bindings \ No newline at end of file diff --git a/typescript-lite/testsuite/src/expected-successes/typescript-compiler.d.ts b/typescript-lite/testsuite/src/expected-successes/typescript-compiler.d.ts new file mode 100644 index 00000000..a3a929c2 --- /dev/null +++ b/typescript-lite/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/typescript-lite/testsuite/tsconfig.json b/typescript-lite/testsuite/tsconfig.json new file mode 100644 index 00000000..f22fb418 --- /dev/null +++ b/typescript-lite/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/tslite-bindings" + ] +} diff --git a/typescript-lite/testsuite/typings.json b/typescript-lite/testsuite/typings.json new file mode 100644 index 00000000..373ac06c --- /dev/null +++ b/typescript-lite/testsuite/typings.json @@ -0,0 +1,6 @@ +{ + "ambientDependencies": { + "jasmine": "registry:dt/jasmine#2.2.0+20160317120654", + "node": "registry:dt/node#4.0.0+20160330064709" + } +}