Коммит
67635a6feb
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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",
|
||||
|
|
Загрузка…
Ссылка в новой задаче