initial Crystal support
This commit is contained in:
Родитель
0cc77f9164
Коммит
9af65fa8bf
|
@ -22,7 +22,7 @@ ENV PATH="${workdir}/swift-4.1.3-RELEASE-ubuntu16.04/usr/bin:${PATH}"
|
|||
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
|
||||
|
||||
# Add .NET core package sources
|
||||
RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
|
||||
RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
|
||||
RUN mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
|
||||
RUN sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main" > /etc/apt/sources.list.d/dotnetdev.list'
|
||||
|
||||
|
@ -65,6 +65,12 @@ RUN sh -c 'curl https://storage.googleapis.com/download.dartlang.org/linux/debia
|
|||
RUN apt-get update
|
||||
RUN apt-get install dart
|
||||
|
||||
# Crystal
|
||||
RUN curl -sL "https://keybase.io/crystal/pgp_keys.asc" | apt-key add -
|
||||
RUN echo "deb https://dist.crystal-lang.org/apt crystal main" | tee /etc/apt/sources.list.d/crystal.list
|
||||
RUN apt-get update
|
||||
RUN apt-get install crystal --assume-yes
|
||||
|
||||
ENV PATH="${workdir}/node_modules/.bin:${PATH}"
|
||||
|
||||
COPY . .
|
||||
|
|
|
@ -174,6 +174,7 @@ files, URLs, or add other options.
|
|||
|
||||
`quicktype` has many complex test dependencies:
|
||||
|
||||
- `crystal` compiler
|
||||
- `dotnetcore` SDK
|
||||
- Java, Maven
|
||||
- `elm` tools
|
||||
|
|
|
@ -71,3 +71,4 @@ export { ElmTargetLanguage, ElmRenderer } from "./language/Elm";
|
|||
export { JSONSchemaTargetLanguage, JSONSchemaRenderer } from "./language/JSONSchema";
|
||||
export { RustTargetLanguage, RustRenderer } from "./language/Rust";
|
||||
export { RubyTargetLanguage, RubyRenderer } from "./language/ruby";
|
||||
export { CrystalTargetLanguage, CrystalRenderer } from "./language/Crystal";
|
||||
|
|
|
@ -14,6 +14,7 @@ import { KotlinTargetLanguage } from "./Kotlin";
|
|||
import { ElmTargetLanguage } from "./Elm";
|
||||
import { JSONSchemaTargetLanguage } from "./JSONSchema";
|
||||
import { RustTargetLanguage } from "./Rust";
|
||||
import { CrystalTargetLanguage } from "./Crystal";
|
||||
import { RubyTargetLanguage } from "./ruby";
|
||||
import { DartTargetLanguage } from "./Dart";
|
||||
import { PythonTargetLanguage } from "./Python";
|
||||
|
@ -22,6 +23,7 @@ export const all: TargetLanguage[] = [
|
|||
new NewtonsoftCSharpTargetLanguage(),
|
||||
new GoTargetLanguage(),
|
||||
new RustTargetLanguage(),
|
||||
new CrystalTargetLanguage(),
|
||||
new CPlusPlusTargetLanguage(),
|
||||
new ObjectiveCTargetLanguage(),
|
||||
new JavaTargetLanguage(),
|
||||
|
|
|
@ -0,0 +1,417 @@
|
|||
import { mapFirst } from "collection-utils";
|
||||
|
||||
import { TargetLanguage } from "../TargetLanguage";
|
||||
import { ConvenienceRenderer, ForbiddenWordsInfo } from "../ConvenienceRenderer";
|
||||
import {
|
||||
legalizeCharacters,
|
||||
splitIntoWords,
|
||||
isLetterOrUnderscoreOrDigit,
|
||||
combineWords,
|
||||
allLowerWordStyle,
|
||||
firstUpperWordStyle,
|
||||
intToHex,
|
||||
utf32ConcatMap,
|
||||
escapeNonPrintableMapper,
|
||||
isPrintable,
|
||||
isAscii,
|
||||
isLetterOrUnderscore
|
||||
} from "../support/Strings";
|
||||
import { Name, Namer, funPrefixNamer } from "../Naming";
|
||||
import { UnionType, Type, ClassType, EnumType } from "../Type";
|
||||
import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils";
|
||||
import { Sourcelike, maybeAnnotated } from "../Source";
|
||||
import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation";
|
||||
import { Option } from "../RendererOptions";
|
||||
import { defined } from "../support/Support";
|
||||
import { RenderContext } from "../Renderer";
|
||||
|
||||
export class CrystalTargetLanguage extends TargetLanguage {
|
||||
protected makeRenderer(renderContext: RenderContext): CrystalRenderer {
|
||||
return new CrystalRenderer(this, renderContext);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super("crystal", ["crystal", "cr", "crystallang"], "cr");
|
||||
}
|
||||
|
||||
protected get defaultIndentation(): string {
|
||||
return " ";
|
||||
}
|
||||
|
||||
protected getOptions(): Option<any>[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const keywords = [
|
||||
"Any",
|
||||
"Array",
|
||||
"Atomic",
|
||||
"Bool",
|
||||
"Channel",
|
||||
"Char",
|
||||
"Class",
|
||||
"Enum",
|
||||
"Enumerable",
|
||||
"Event",
|
||||
"Extern",
|
||||
"Exception",
|
||||
"File",
|
||||
"Float",
|
||||
"Float32",
|
||||
"Float64",
|
||||
"GC",
|
||||
"GZip",
|
||||
"Hash",
|
||||
"HTML",
|
||||
"HTTP",
|
||||
"Int",
|
||||
"Int128",
|
||||
"Int16",
|
||||
"Int32",
|
||||
"Int64",
|
||||
"Int8",
|
||||
"Iterable",
|
||||
"Link",
|
||||
"Logger",
|
||||
"Math",
|
||||
"Mutex",
|
||||
"Nil",
|
||||
"Number",
|
||||
"JSON",
|
||||
"IO",
|
||||
"Object",
|
||||
"Pointer",
|
||||
"Proc",
|
||||
"Process",
|
||||
"Range",
|
||||
"Random",
|
||||
"Regex",
|
||||
"Reference",
|
||||
"Set",
|
||||
"Signal",
|
||||
"Slice",
|
||||
"Spec",
|
||||
"StaticArray",
|
||||
"String",
|
||||
"Struct",
|
||||
"Symbol",
|
||||
"System",
|
||||
"TCPServer",
|
||||
"TCPSocket",
|
||||
"Socket",
|
||||
"Tempfile",
|
||||
"Termios",
|
||||
"Time",
|
||||
"Tuple",
|
||||
"ThreadLocal",
|
||||
"UDPSocket",
|
||||
"UInt128",
|
||||
"UInt16",
|
||||
"UInt32",
|
||||
"UInt64",
|
||||
"UInt8",
|
||||
"Union",
|
||||
"UNIXServer",
|
||||
"UNIXSocket",
|
||||
"UUID",
|
||||
"URI",
|
||||
"VaList",
|
||||
"Value",
|
||||
"Void",
|
||||
"WeakRef",
|
||||
"XML",
|
||||
"YAML",
|
||||
"Zip",
|
||||
"Zlib",
|
||||
"abstract",
|
||||
"alias",
|
||||
"as",
|
||||
"as?",
|
||||
"asm",
|
||||
"begin",
|
||||
"break",
|
||||
"case",
|
||||
"class",
|
||||
"def",
|
||||
"do",
|
||||
"else",
|
||||
"elsif",
|
||||
"end",
|
||||
"ensure",
|
||||
"enum",
|
||||
"extend",
|
||||
"false",
|
||||
"for",
|
||||
"fun",
|
||||
"if",
|
||||
"in",
|
||||
"include",
|
||||
"instance_sizeof",
|
||||
"is_a?",
|
||||
"lib",
|
||||
"macro",
|
||||
"module",
|
||||
"next",
|
||||
"nil",
|
||||
"nil?",
|
||||
"of",
|
||||
"out",
|
||||
"pointerof",
|
||||
"private",
|
||||
"protected",
|
||||
"require",
|
||||
"rescue",
|
||||
"return",
|
||||
"select",
|
||||
"self",
|
||||
"sizeof",
|
||||
"struct",
|
||||
"super",
|
||||
"then",
|
||||
"true",
|
||||
"type",
|
||||
"typeof",
|
||||
"uninitialized",
|
||||
"union",
|
||||
"unless",
|
||||
"until",
|
||||
"when",
|
||||
"while",
|
||||
"with",
|
||||
"yield"
|
||||
];
|
||||
|
||||
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 crystalStyle(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) => crystalStyle(original, true));
|
||||
const camelNamingFunction = funPrefixNamer("camel", (original: string) => crystalStyle(original, false));
|
||||
|
||||
const standardUnicodeCrystalEscape = (codePoint: number): string => {
|
||||
if (codePoint <= 0xffff) {
|
||||
return "\\u{" + intToHex(codePoint, 4) + "}";
|
||||
} else {
|
||||
return "\\u{" + intToHex(codePoint, 6) + "}";
|
||||
}
|
||||
};
|
||||
|
||||
const crystalStringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, standardUnicodeCrystalEscape));
|
||||
|
||||
export class CrystalRenderer extends ConvenienceRenderer {
|
||||
constructor(targetLanguage: TargetLanguage, renderContext: RenderContext) {
|
||||
super(targetLanguage, renderContext);
|
||||
}
|
||||
|
||||
protected makeNamedTypeNamer(): Namer {
|
||||
return camelNamingFunction;
|
||||
}
|
||||
|
||||
protected namerForObjectProperty(): Namer | null {
|
||||
return snakeNamingFunction;
|
||||
}
|
||||
|
||||
protected makeUnionMemberNamer(): Namer | null {
|
||||
return camelNamingFunction;
|
||||
}
|
||||
|
||||
protected makeEnumCaseNamer(): Namer | null {
|
||||
return camelNamingFunction;
|
||||
}
|
||||
|
||||
protected forbiddenNamesForGlobalNamespace(): string[] {
|
||||
return keywords;
|
||||
}
|
||||
|
||||
protected forbiddenForObjectProperties(_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 get commentLineStart(): string {
|
||||
return "# ";
|
||||
}
|
||||
|
||||
private nullableCrystalType = (t: Type, withIssues: boolean): Sourcelike => {
|
||||
return [this.crystalType(t, withIssues), "?"];
|
||||
};
|
||||
|
||||
protected isImplicitCycleBreaker(t: Type): boolean {
|
||||
const kind = t.kind;
|
||||
return kind === "array" || kind === "map";
|
||||
}
|
||||
|
||||
private crystalType = (t: Type, withIssues: boolean = false): Sourcelike => {
|
||||
return matchType<Sourcelike>(
|
||||
t,
|
||||
_anyType => maybeAnnotated(withIssues, anyTypeIssueAnnotation, "JSON::Any"),
|
||||
_nullType => maybeAnnotated(withIssues, nullTypeIssueAnnotation, "Nil"),
|
||||
_boolType => "Bool",
|
||||
_integerType => "Int32",
|
||||
_doubleType => "Float64",
|
||||
_stringType => "String",
|
||||
arrayType => ["Array(", this.crystalType(arrayType.items, withIssues), ")"],
|
||||
classType => this.nameForNamedType(classType),
|
||||
mapType => ["Hash(String, ", this.crystalType(mapType.values, withIssues), ")"],
|
||||
_enumType => "String",
|
||||
unionType => {
|
||||
const nullable = nullableFromUnion(unionType);
|
||||
|
||||
if (nullable !== null) return this.nullableCrystalType(nullable, withIssues);
|
||||
|
||||
const [hasNull] = removeNullFromUnion(unionType);
|
||||
|
||||
const name = this.nameForNamedType(unionType);
|
||||
|
||||
return hasNull !== null ? ([name, "?"] as Sourcelike) : name;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
private breakCycle = (t: Type, withIssues: boolean): any => {
|
||||
const crystalType = this.crystalType(t, withIssues);
|
||||
return crystalType;
|
||||
};
|
||||
|
||||
private emitRenameAttribute(propName: Name, jsonName: string) {
|
||||
const escapedName = crystalStringEscape(jsonName);
|
||||
const namesDiffer = this.sourcelikeToString(propName) !== escapedName;
|
||||
if (namesDiffer) {
|
||||
this.emitLine('@[JSON::Field(key: "', escapedName, '")]');
|
||||
}
|
||||
}
|
||||
|
||||
protected emitStructDefinition(c: ClassType, className: Name): void {
|
||||
this.emitDescription(this.descriptionForType(c));
|
||||
|
||||
const structBody = () =>
|
||||
this.forEachClassProperty(c, "none", (name, jsonName, prop) => {
|
||||
this.emitDescription(this.descriptionForClassProperty(c, jsonName));
|
||||
this.emitRenameAttribute(name, jsonName);
|
||||
this.emitLine("property ", name, " : ", this.crystalType(prop.type, true));
|
||||
});
|
||||
|
||||
this.emitBlock(["class ", className], structBody);
|
||||
}
|
||||
|
||||
protected emitBlock(line: Sourcelike, f: () => void): void {
|
||||
this.emitLine(line, "");
|
||||
this.indent(() => {
|
||||
this.emitLine("include JSON::Serializable");
|
||||
});
|
||||
this.ensureBlankLine();
|
||||
this.indent(f);
|
||||
this.emitLine("end");
|
||||
}
|
||||
|
||||
protected emitEnum(line: Sourcelike, f: () => void): void {
|
||||
this.emitLine(line);
|
||||
this.indent(f);
|
||||
this.emitLine("end");
|
||||
}
|
||||
|
||||
protected emitUnion(u: UnionType, unionName: Name): void {
|
||||
const isMaybeWithSingleType = nullableFromUnion(u);
|
||||
|
||||
if (isMaybeWithSingleType !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emitDescription(this.descriptionForType(u));
|
||||
|
||||
const [, nonNulls] = removeNullFromUnion(u);
|
||||
|
||||
let count = nonNulls.size;
|
||||
|
||||
let types: Sourcelike[][] = [];
|
||||
this.emitLine(["alias ", unionName, " = "]);
|
||||
this.forEachUnionMember(u, nonNulls, "none", null, (_fieldName, t) => {
|
||||
const last = --count === 0;
|
||||
const blanksOrPipe = last ? "" : " |";
|
||||
const crystalType = this.breakCycle(t, true);
|
||||
this.emitLine([crystalType, blanksOrPipe]);
|
||||
types.push(crystalType);
|
||||
});
|
||||
}
|
||||
|
||||
protected emitTopLevelAlias(t: Type, name: Name): void {
|
||||
this.emitLine("alias ", name, " = ", this.crystalType(t));
|
||||
}
|
||||
|
||||
protected emitLeadingComments(): void {
|
||||
if (this.leadingComments !== undefined) {
|
||||
this.emitCommentLines(this.leadingComments);
|
||||
return;
|
||||
}
|
||||
|
||||
const topLevelName = defined(mapFirst(this.topLevels));
|
||||
this.emitMultiline(
|
||||
`# Example code that deserializes and serializes the model.
|
||||
# class ${topLevelName}
|
||||
# include JSON::Serializable
|
||||
#
|
||||
# @[JSON::Field(key: "answer")]
|
||||
# property answer : Int32
|
||||
# end
|
||||
#
|
||||
# ${topLevelName}.from_json(%({"answer": 42}))
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
protected emitSourceStructure(): void {
|
||||
this.emitLeadingComments();
|
||||
this.ensureBlankLine();
|
||||
this.emitLine('require "json"');
|
||||
|
||||
this.forEachTopLevel(
|
||||
"leading",
|
||||
(t, name) => this.emitTopLevelAlias(t, name),
|
||||
t => this.namedTypeToNameForTopLevel(t) === undefined
|
||||
);
|
||||
|
||||
this.forEachObject("leading-and-interposing", (c: ClassType, name: Name) => this.emitStructDefinition(c, name));
|
||||
this.forEachUnion("leading-and-interposing", (u, name) => this.emitUnion(u, name));
|
||||
}
|
||||
}
|
|
@ -683,6 +683,7 @@ class GraphQLFixture extends LanguageFixture {
|
|||
}
|
||||
|
||||
export const allFixtures: Fixture[] = [
|
||||
new JSONFixture(languages.CrystalLanguage),
|
||||
new JSONFixture(languages.CSharpLanguage),
|
||||
new JSONFixture(languages.JavaLanguage),
|
||||
new JSONFixture(languages.GoLanguage),
|
||||
|
@ -700,6 +701,7 @@ export const allFixtures: Fixture[] = [
|
|||
new JSONFixture(languages.DartLanguage),
|
||||
new JSONSchemaJSONFixture(languages.CSharpLanguage),
|
||||
new JSONTypeScriptFixture(languages.CSharpLanguage),
|
||||
new JSONSchemaFixture(languages.CrystalLanguage),
|
||||
new JSONSchemaFixture(languages.CSharpLanguage),
|
||||
new JSONSchemaFixture(languages.JavaLanguage),
|
||||
new JSONSchemaFixture(languages.GoLanguage),
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
require "json"
|
||||
require "./TopLevel"
|
||||
|
||||
json = File.read(ARGV[0].not_nil!)
|
||||
top = TopLevel.from_json(json)
|
||||
|
||||
puts top.to_json
|
|
@ -181,6 +181,39 @@ export const RustLanguage: Language = {
|
|||
sourceFiles: ["src/language/Rust.ts"]
|
||||
};
|
||||
|
||||
export const CrystalLanguage: Language = {
|
||||
name: "crystal",
|
||||
base: "test/fixtures/crystal",
|
||||
compileCommand: "crystal build -o quicktype main.cr",
|
||||
runCommand(sample: string) {
|
||||
return `./quicktype "${sample}"`;
|
||||
},
|
||||
diffViaSchema: false,
|
||||
skipDiffViaSchema: [],
|
||||
allowMissingNull: true,
|
||||
features: ["enum", "union", "no-defaults"],
|
||||
output: "TopLevel.cr",
|
||||
topLevel: "TopLevel",
|
||||
skipJSON: [
|
||||
"blns-object.json",
|
||||
"identifiers.json",
|
||||
"simple-identifiers.json",
|
||||
"bug427.json",
|
||||
"nst-test-suite.json",
|
||||
"34702.json",
|
||||
"34702.json",
|
||||
"4961a.json",
|
||||
"32431.json",
|
||||
"68c30.json",
|
||||
"e8b04.json"
|
||||
],
|
||||
skipSchema: [],
|
||||
skipMiscJSON: false,
|
||||
rendererOptions: {},
|
||||
quickTestRendererOptions: [],
|
||||
sourceFiles: ["src/language/Crystal.ts"]
|
||||
};
|
||||
|
||||
export const RubyLanguage: Language = {
|
||||
name: "ruby",
|
||||
base: "test/fixtures/ruby",
|
||||
|
|
Загрузка…
Ссылка в новой задаче