Merge pull request #512 from krk/master

Rust language support added.
This commit is contained in:
David Siegel 2018-02-13 07:58:30 -08:00 коммит произвёл GitHub
Родитель 8cc9ffad5d 794768edc1
Коммит 67635a6feb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 475 добавлений и 1 удалений

Просмотреть файл

@ -5,6 +5,7 @@ env:
- FIXTURE=golang,cplusplus,schema,graphql
- FIXTURE=swift,java,schema-json-csharp
- FIXTURE=elm,typescript,csharp
- FIXTURE=rust
services:
- docker
before_install:

Просмотреть файл

@ -29,6 +29,10 @@ RUN apt-get install dotnet-sdk-2.0.0 --assume-yes
# Install Boost for C++
RUN apt-get install libboost-all-dev --assume-yes
# Install Rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
ENV PATH="${workdir}/node_modules/.bin:${PATH}"
COPY . .

Просмотреть файл

@ -145,6 +145,7 @@ files, URLs, or add other options.
* `golang` stack
* `swift` compiler
* `clang` and Objective-C Foundation (must be tested separately on macOS)
* `rust` tools
We've assembled all of these tools in a Docker container that you build and test within:

Просмотреть файл

@ -13,7 +13,8 @@ import {
matchTypeExhaustive,
TypeKind,
isNamedType,
ClassProperty
ClassProperty,
MapType
} from "./Type";
import { Namespace, Name, Namer, FixedName, SimpleName, DependencyName, keywordNamespace } from "./Naming";
import { Renderer, BlankLineLocations } from "./Renderer";
@ -45,6 +46,7 @@ export abstract class ConvenienceRenderer extends Renderer {
private _namedEnums: OrderedSet<EnumType>;
private _namedUnions: OrderedSet<UnionType>;
private _haveUnions: boolean;
private _haveMaps: boolean;
private _haveOptionalProperties: boolean;
private _cycleBreakerTypes?: Set<Type>;
@ -339,6 +341,10 @@ export abstract class ConvenienceRenderer extends Renderer {
return this._haveUnions;
}
protected get haveMaps(): boolean {
return this._haveMaps;
}
protected get haveOptionalProperties(): boolean {
return this._haveOptionalProperties;
}
@ -580,6 +586,7 @@ export abstract class ConvenienceRenderer extends Renderer {
const types = this.typeGraph.allTypesUnordered();
this._haveUnions = types.some(t => t instanceof UnionType);
this._haveMaps = types.some(t => t instanceof MapType);
this._haveOptionalProperties = types
.filter(t => t instanceof ClassType)
.some(c => (c as ClassType).properties.some(p => p.isOptional));

Просмотреть файл

@ -12,10 +12,12 @@ import TypeScriptTargetLanguage from "./TypeScript";
import SwiftTargetLanguage from "./Swift";
import ElmTargetLanguage from "./Elm";
import JSONSchemaTargetLanguage from "./JSONSchema";
import RustTargetLanguage from "./Rust";
export const all: TargetLanguage[] = [
new CSharpTargetLanguage(),
new GoTargetLanguage(),
new RustTargetLanguage(),
new CPlusPlusTargetLanguage(),
new ObjectiveCTargetLanguage(),
new JavaTargetLanguage(),

336
src/Language/Rust.ts Normal file
Просмотреть файл

@ -0,0 +1,336 @@
import { TargetLanguage } from "../TargetLanguage";
import { TypeGraph } from "../TypeGraph";
import { ConvenienceRenderer, ForbiddenWordsInfo } from "../ConvenienceRenderer";
import {
legalizeCharacters,
splitIntoWords,
isLetterOrUnderscoreOrDigit,
combineWords,
allLowerWordStyle,
firstUpperWordStyle,
intToHex,
utf32ConcatMap,
escapeNonPrintableMapper,
isPrintable,
isAscii,
isLetterOrUnderscore
} from "../Strings";
import { Name, Namer, funPrefixNamer } from "../Naming";
import { UnionType, nullableFromUnion, Type, ClassType, matchType, removeNullFromUnion, EnumType } from "../Type";
import { Sourcelike, maybeAnnotated } from "../Source";
import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation";
export default class RustTargetLanguage extends TargetLanguage {
protected get rendererClass(): new (graph: TypeGraph, ...optionValues: any[]) => ConvenienceRenderer {
return RustRenderer;
}
constructor() {
super("Rust", ["rs", "rust", "rustlang"], "rs");
this.setOptions([]);
}
}
const keywords = [
// Special reserved identifiers used internally for elided lifetimes,
// unnamed method parameters, crate root module, error recovery etc.
"{{root}}",
"$crate",
// Keywords used in the language.
"as",
"box",
"break",
"const",
"continue",
"crate",
"else",
"enum",
"extern",
"false",
"fn",
"for",
"if",
"impl",
"in",
"let",
"loop",
"match",
"mod",
"move",
"mut",
"pub",
"ref",
"return",
"self",
"Self",
"static",
"struct",
"super",
"trait",
"true",
"type",
"unsafe",
"use",
"where",
"while",
// Keywords reserved for future use.
"abstract",
"alignof",
"become",
"do",
"final",
"macro",
"offsetof",
"override",
"priv",
"proc",
"pure",
"sizeof",
"typeof",
"unsized",
"virtual",
"yield",
// Weak keywords, have special meaning only in specific contexts.
"catch",
"default",
"dyn",
"'static",
"union"
];
const isAsciiLetterOrUnderscoreOrDigit = (codePoint: number): boolean => {
if (!isAscii(codePoint)) {
return false;
}
return isLetterOrUnderscoreOrDigit(codePoint);
};
const isAsciiLetterOrUnderscore = (codePoint: number): boolean => {
if (!isAscii(codePoint)) {
return false;
}
return isLetterOrUnderscore(codePoint);
};
const legalizeName = legalizeCharacters(isAsciiLetterOrUnderscoreOrDigit);
function rustStyle(original: string, isSnakeCase: boolean): string {
const words = splitIntoWords(original);
const wordStyle = isSnakeCase ? allLowerWordStyle : firstUpperWordStyle;
const combined = combineWords(
words,
legalizeName,
wordStyle,
wordStyle,
wordStyle,
wordStyle,
isSnakeCase ? "_" : "",
isAsciiLetterOrUnderscore
);
return combined === "_" ? "_underscore" : combined;
}
const snakeNamingFunction = funPrefixNamer("default", (original: string) => rustStyle(original, true));
const camelNamingFunction = funPrefixNamer("camel", (original: string) => rustStyle(original, false));
const standardUnicodeRustEscape = (codePoint: number): string => {
if (codePoint <= 0xffff) {
return "\\u{" + intToHex(codePoint, 4) + "}";
} else {
return "\\u{" + intToHex(codePoint, 6) + "}";
}
};
const rustStringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, standardUnicodeRustEscape));
class RustRenderer extends ConvenienceRenderer {
protected makeNamedTypeNamer(): Namer {
return camelNamingFunction;
}
protected namerForClassProperty(): Namer | null {
return snakeNamingFunction;
}
protected makeUnionMemberNamer(): Namer | null {
return camelNamingFunction;
}
protected makeEnumCaseNamer(): Namer | null {
return camelNamingFunction;
}
protected get forbiddenNamesForGlobalNamespace(): string[] {
return keywords;
}
protected forbiddenForClassProperties(_c: ClassType, _className: Name): ForbiddenWordsInfo {
return { names: [], includeGlobalForbidden: true };
}
protected forbiddenForUnionMembers(_u: UnionType, _unionName: Name): ForbiddenWordsInfo {
return { names: [], includeGlobalForbidden: true };
}
protected forbiddenForEnumCases(_e: EnumType, _enumName: Name): ForbiddenWordsInfo {
return { names: [], includeGlobalForbidden: true };
}
protected topLevelNameStyle(rawName: string): string {
return rustStyle(rawName, false);
}
private nullableRustType = (t: Type, withIssues: boolean): Sourcelike => {
return ["Option<", this.breakCycle(t, withIssues), ">"];
};
protected isImplicitCycleBreaker(t: Type): boolean {
const kind = t.kind;
return kind === "array" || kind === "map";
}
private rustType = (t: Type, withIssues: boolean = false): Sourcelike => {
return matchType<Sourcelike>(
t,
_anyType => maybeAnnotated(withIssues, anyTypeIssueAnnotation, "serde_json::Value"),
_nullType => maybeAnnotated(withIssues, nullTypeIssueAnnotation, "Option<serde_json::Value>"),
_boolType => "bool",
_integerType => "i64",
_doubleType => "f64",
_stringType => "String",
arrayType => ["Vec<", this.rustType(arrayType.items, withIssues), ">"],
classType => this.nameForNamedType(classType),
mapType => ["HashMap<String, ", this.rustType(mapType.values, withIssues), ">"],
enumType => this.nameForNamedType(enumType),
unionType => {
const nullable = nullableFromUnion(unionType);
if (nullable) return this.nullableRustType(nullable, withIssues);
const [hasNull] = removeNullFromUnion(unionType);
const isCycleBreaker = this.isCycleBreakerType(unionType);
const name = isCycleBreaker
? ["Box<", this.nameForNamedType(unionType), ">"]
: this.nameForNamedType(unionType);
return hasNull ? (["Option<", name, ">"] as Sourcelike) : name;
}
);
};
private breakCycle = (t: Type, withIssues: boolean): any => {
const rustType = this.rustType(t, withIssues);
const isCycleBreaker = this.isCycleBreakerType(t);
return isCycleBreaker ? ["Box<", rustType, ">"] : rustType;
};
private emitStructDefinition = (c: ClassType, className: Name): void => {
this.emitLine("#[derive(Serialize, Deserialize)]");
const structBody = () =>
this.forEachClassProperty(c, "none", (name, jsonName, prop) => {
const escapedName = rustStringEscape(jsonName);
this.emitLine('#[serde(rename = "', escapedName, '")]');
this.emitLine(name, ": ", this.breakCycle(prop.type, true), ",");
});
this.emitBlock(["pub struct ", className], structBody);
};
private emitBlock = (line: Sourcelike, f: () => void): void => {
this.emitLine(line, " {");
this.indent(f);
this.emitLine("}");
};
private emitUnion = (u: UnionType, unionName: Name): void => {
const isMaybeWithSingleType = nullableFromUnion(u);
if (isMaybeWithSingleType !== null) {
return;
}
this.emitLine("#[derive(Serialize, Deserialize)]");
this.emitLine("#[serde(untagged)]");
const [, nonNulls] = removeNullFromUnion(u);
this.emitBlock(["pub enum ", unionName], () =>
this.forEachUnionMember(u, nonNulls, "none", null, (fieldName, t) => {
const rustType = this.breakCycle(t, true);
this.emitLine([fieldName, "(", rustType, "),"]);
})
);
};
emitEnumDefinition = (e: EnumType, enumName: Name): void => {
this.emitLine("#[derive(Serialize, Deserialize)]");
this.emitBlock(["pub enum ", enumName], () =>
this.forEachEnumCase(e, "none", (name, jsonName) => {
const escapedName = rustStringEscape(jsonName);
this.emitLine('#[serde(rename = "', escapedName, '")]');
this.emitLine([name, ","]);
})
);
};
emitTopLevelAlias = (t: Type, name: Name): void => {
this.emitLine("pub type ", name, " = ", this.rustType(t), ";");
};
protected emitUsageExample(): void {
this.emitMultiline(
`/* Example code that deserializes and serializes the model.
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
use generated_module::TopLevel;
fn main() {
let json = "{ answer: 42 }";
let top_level: TopLevel = serde_json::from_str(&json).unwrap();
let result = serde_json::to_string(&top_level).unwrap();
println!("{}", &result);
}*/`
);
}
protected emitSourceStructure(): void {
this.emitUsageExample();
this.emitLine();
this.emitLine("extern crate serde_json;");
if (this.haveMaps) {
this.emitLine("use std::collections::HashMap;");
}
this.forEachTopLevel("leading", this.emitTopLevelAlias, t => this.namedTypeToNameForTopLevel(t) === undefined);
this.forEachClass("leading-and-interposing", this.emitStructDefinition);
this.forEachUnion("leading-and-interposing", this.emitUnion);
this.forEachEnum("leading-and-interposing", this.emitEnumDefinition);
}
}

Просмотреть файл

@ -225,6 +225,10 @@ export function isNumeric(codePoint: number): boolean {
return ["No", "Nd", "Nl"].indexOf(category) >= 0;
}
export function isLetterOrDigit(codePoint: number): boolean {
return isLetter(codePoint) || isDigit(codePoint);
}
export function isLetterOrUnderscore(codePoint: number): boolean {
return isLetter(codePoint) || codePoint === 0x5f;
}

Просмотреть файл

@ -549,6 +549,7 @@ export const allFixtures: Fixture[] = [
new JSONFixture(languages.JavaLanguage),
new JSONFixture(languages.GoLanguage),
new JSONFixture(languages.CPlusPlusLanguage),
new JSONFixture(languages.RustLanguage),
new JSONFixture(languages.ElmLanguage),
new JSONFixture(languages.SwiftLanguage),
new JSONFixture(languages.ObjectiveCLanguage),
@ -558,6 +559,7 @@ export const allFixtures: Fixture[] = [
new JSONSchemaFixture(languages.JavaLanguage),
new JSONSchemaFixture(languages.GoLanguage),
new JSONSchemaFixture(languages.CPlusPlusLanguage),
new JSONSchemaFixture(languages.RustLanguage),
new JSONSchemaFixture(languages.ElmLanguage),
new JSONSchemaFixture(languages.SwiftLanguage),
new JSONSchemaFixture(languages.TypeScriptLanguage),

53
test/fixtures/rust/Cargo.lock сгенерированный поставляемый Normal file
Просмотреть файл

@ -0,0 +1,53 @@
[[package]]
name = "dtoa"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "itoa"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "main"
version = "0.0.1"
dependencies = [
"serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-traits"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-traits"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_json"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
"checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c"
"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
"checksum num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e7de20f146db9d920c45ee8ed8f71681fd9ade71909b48c3acbd766aa504cf10"
"checksum serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "db99f3919e20faa51bb2996057f5031d8685019b5a06139b1ce761da671b8526"
"checksum serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c9db7266c7d63a4c4b7fe8719656ccdd51acf1bed6124b174f933b009fb10bcb"

14
test/fixtures/rust/Cargo.toml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,14 @@
[package]
name = "quick_type_test"
version = "0.0.1"
[[bin]]
name = "quick_type_test"
path = "main.rs"
test = false
doc = false
[dependencies]
serde_json = "1.0"
serde = "1.0"
serde_derive = "1.0"

31
test/fixtures/rust/main.rs поставляемый Normal file
Просмотреть файл

@ -0,0 +1,31 @@
mod module_under_test;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
use module_under_test::TopLevel;
use std::env;
use std::fs::File;
use std::io::prelude::*;
fn main() {
let args: Vec<String> = env::args().collect();
let filename = &args[1];
let mut f = File::open(filename).expect("Input file not found");
let mut json = String::new();
f.read_to_string(&mut json)
.expect("Input file cannot be read.");
let top_level: TopLevel = serde_json::from_str(&json).unwrap();
let result = serde_json::to_string(&top_level).unwrap();
println!("{}", &result);
}

Просмотреть файл

@ -67,6 +67,25 @@ export const JavaLanguage: Language = {
quickTestRendererOptions: []
};
export const RustLanguage: Language = {
name: "rust",
base: "test/fixtures/rust",
compileCommand: "cargo build",
runCommand(sample: string) {
return `./target/debug/quick_type_test "${sample}"`;
},
// FIXME: implement comparing multiple files
diffViaSchema: false,
allowMissingNull: false,
output: "module_under_test.rs",
topLevel: "TopLevel",
skipJSON: [],
skipSchema: [],
skipMiscJSON: true,
rendererOptions: {},
quickTestRendererOptions: []
};
export const GoLanguage: Language = {
name: "golang",
base: "test/fixtures/golang",