Ruby support
This commit is contained in:
Родитель
4e5c67923c
Коммит
42f4be58c2
|
@ -14,6 +14,7 @@ import SwiftTargetLanguage from "./Swift";
|
|||
import ElmTargetLanguage from "./Elm";
|
||||
import JSONSchemaTargetLanguage from "./JSONSchema";
|
||||
import RustTargetLanguage from "./Rust";
|
||||
import RubyTargetLanguage from "./Ruby";
|
||||
|
||||
export const all: TargetLanguage[] = [
|
||||
new CSharpTargetLanguage(),
|
||||
|
@ -28,6 +29,7 @@ export const all: TargetLanguage[] = [
|
|||
new SwiftTargetLanguage(),
|
||||
new ElmTargetLanguage(),
|
||||
new JSONSchemaTargetLanguage(),
|
||||
new RubyTargetLanguage(),
|
||||
new SimpleTypesTargetLanguage()
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,612 @@
|
|||
"use strict";
|
||||
|
||||
import { snakeCase, includes } from "lodash";
|
||||
const unicode = require("unicode-properties");
|
||||
|
||||
import { TypeGraph } from "../../TypeGraph";
|
||||
import { Sourcelike, modifySource } from "../../Source";
|
||||
import { Namer, Name } from "../../Naming";
|
||||
import { ConvenienceRenderer, ForbiddenWordsInfo } from "../../ConvenienceRenderer";
|
||||
import { TargetLanguage } from "../../TargetLanguage";
|
||||
import { Option, BooleanOption } from "../../RendererOptions";
|
||||
|
||||
import * as keywords from "./keywords";
|
||||
|
||||
import {
|
||||
Type,
|
||||
EnumType,
|
||||
ClassType,
|
||||
nullableFromUnion,
|
||||
matchType,
|
||||
UnionType,
|
||||
ArrayType,
|
||||
MapType,
|
||||
ClassProperty,
|
||||
removeNullFromUnion
|
||||
} from "../../Type";
|
||||
|
||||
import {
|
||||
legalizeCharacters,
|
||||
splitIntoWords,
|
||||
combineWords,
|
||||
firstUpperWordStyle,
|
||||
allUpperWordStyle,
|
||||
allLowerWordStyle,
|
||||
utf32ConcatMap,
|
||||
isPrintable,
|
||||
escapeNonPrintableMapper,
|
||||
intToHex
|
||||
} from "../../Strings";
|
||||
|
||||
function unicodeEscape(codePoint: number): string {
|
||||
return "\\u{" + intToHex(codePoint, 0) + "}";
|
||||
}
|
||||
|
||||
const stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape));
|
||||
|
||||
export default class RubyTargetLanguage extends TargetLanguage {
|
||||
private readonly _justTypesOption = new BooleanOption("just-types", "Plain types only", false);
|
||||
|
||||
constructor() {
|
||||
super("Ruby", ["ruby"], "rb");
|
||||
}
|
||||
|
||||
protected getOptions(): Option<any>[] {
|
||||
return [this._justTypesOption];
|
||||
}
|
||||
|
||||
get supportsOptionalClassProperties(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected get defaultIndentation(): string {
|
||||
return " ";
|
||||
}
|
||||
|
||||
protected get rendererClass(): new (
|
||||
graph: TypeGraph,
|
||||
leadingComments: string[] | undefined,
|
||||
...optionValues: any[]
|
||||
) => ConvenienceRenderer {
|
||||
return RubyRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
function isStartCharacter(utf16Unit: number): boolean {
|
||||
return unicode.isAlphabetic(utf16Unit) || utf16Unit === 0x5f; // underscore
|
||||
}
|
||||
|
||||
function isPartCharacter(utf16Unit: number): boolean {
|
||||
const category: string = unicode.getCategory(utf16Unit);
|
||||
return includes(["Nd", "Pc", "Mn", "Mc"], category) || isStartCharacter(utf16Unit);
|
||||
}
|
||||
|
||||
const legalizeName = legalizeCharacters(isPartCharacter);
|
||||
|
||||
function simpleNameStyle(original: string, uppercase: boolean): string {
|
||||
const words = splitIntoWords(original);
|
||||
return combineWords(
|
||||
words,
|
||||
legalizeName,
|
||||
uppercase ? firstUpperWordStyle : allLowerWordStyle,
|
||||
uppercase ? firstUpperWordStyle : allLowerWordStyle,
|
||||
allUpperWordStyle,
|
||||
allUpperWordStyle,
|
||||
"",
|
||||
isStartCharacter
|
||||
);
|
||||
}
|
||||
|
||||
function memberNameStyle(original: string): string {
|
||||
const words = splitIntoWords(original);
|
||||
return combineWords(
|
||||
words,
|
||||
legalizeName,
|
||||
allLowerWordStyle,
|
||||
allLowerWordStyle,
|
||||
allLowerWordStyle,
|
||||
allLowerWordStyle,
|
||||
"_",
|
||||
isStartCharacter
|
||||
);
|
||||
}
|
||||
|
||||
class RubyRenderer extends ConvenienceRenderer {
|
||||
constructor(graph: TypeGraph, leadingComments: string[] | undefined, private readonly _justTypes: boolean) {
|
||||
super(graph, leadingComments);
|
||||
}
|
||||
|
||||
protected get commentLineStart(): string {
|
||||
return "# ";
|
||||
}
|
||||
|
||||
protected get needsTypeDeclarationBeforeUse(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected canBeForwardDeclared(t: Type): boolean {
|
||||
return "class" === t.kind;
|
||||
}
|
||||
|
||||
protected topLevelNameStyle(rawName: string): string {
|
||||
return simpleNameStyle(rawName, true);
|
||||
}
|
||||
|
||||
protected forbiddenNamesForGlobalNamespace(): string[] {
|
||||
return keywords.globals.concat(["Types", "JSON", "Dry", "Constructor"]);
|
||||
}
|
||||
|
||||
protected forbiddenForClassProperties(_c: ClassType, _classNamed: Name): ForbiddenWordsInfo {
|
||||
return { names: keywords.reservedProperties, includeGlobalForbidden: true };
|
||||
}
|
||||
|
||||
protected makeNamedTypeNamer(): Namer {
|
||||
return new Namer("types", n => simpleNameStyle(n, true), []);
|
||||
}
|
||||
|
||||
protected namerForClassProperty(): Namer {
|
||||
return new Namer("properties", memberNameStyle, []);
|
||||
}
|
||||
|
||||
protected makeUnionMemberNamer(): Namer {
|
||||
return new Namer("properties", memberNameStyle, []);
|
||||
}
|
||||
|
||||
protected makeEnumCaseNamer(): Namer {
|
||||
return new Namer("enum-cases", n => simpleNameStyle(n, true), []);
|
||||
}
|
||||
|
||||
private dryType(t: Type, isOptional: boolean = false): Sourcelike {
|
||||
const optional = isOptional ? ".optional" : "";
|
||||
return matchType<Sourcelike>(
|
||||
t,
|
||||
_anyType => ["Types::Any", optional],
|
||||
_nullType => ["Types::Strict::Nil", optional],
|
||||
_boolType => ["Types::Strict::Bool", optional],
|
||||
_integerType => ["Types::Strict::Int", optional],
|
||||
_doubleType => ["Types::StrictDouble", optional],
|
||||
_stringType => ["Types::Strict::String", optional],
|
||||
arrayType => ["Types.Array(", this.dryType(arrayType.items), ")", optional],
|
||||
classType => ["Types.Instance(", this.nameForNamedType(classType), ")", optional],
|
||||
mapType => ["Types::Strict::Hash.meta(of: ", this.dryType(mapType.values), ")", optional],
|
||||
enumType => ["Types::", this.nameForNamedType(enumType), optional],
|
||||
unionType => {
|
||||
const nullable = nullableFromUnion(unionType);
|
||||
if (nullable !== null) {
|
||||
return [this.dryType(nullable), ".optional"];
|
||||
}
|
||||
return ["Types.Instance(", this.nameForNamedType(unionType), ")", optional];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private exampleUse(t: Type, exp: Sourcelike, depth: number = 6, optional: boolean = false): Sourcelike {
|
||||
if (depth-- <= 0) {
|
||||
return exp;
|
||||
}
|
||||
|
||||
const safeNav = optional ? "&" : "";
|
||||
|
||||
return matchType<Sourcelike>(
|
||||
t,
|
||||
_anyType => exp,
|
||||
_nullType => [exp, ".nil?"],
|
||||
_boolType => exp,
|
||||
_integerType => [exp, ".even?"],
|
||||
_doubleType => exp,
|
||||
_stringType => exp,
|
||||
arrayType => this.exampleUse(arrayType.items, [exp, safeNav, ".first"], depth),
|
||||
classType => {
|
||||
let info: { name: Name; prop: ClassProperty } | undefined;
|
||||
this.forEachClassProperty(classType, "none", (name, _json, prop) => {
|
||||
if (includes(["class", "map", "array"], prop.type.kind)) {
|
||||
info = { name, prop };
|
||||
} else {
|
||||
info = info || { name, prop };
|
||||
}
|
||||
});
|
||||
if (info !== undefined) {
|
||||
return this.exampleUse(info.prop.type, [exp, safeNav, ".", info.name], depth, info.prop.isOptional);
|
||||
}
|
||||
return exp;
|
||||
},
|
||||
mapType => this.exampleUse(mapType.values, [exp, safeNav, `["…"]`], depth),
|
||||
enumType => {
|
||||
let name: Name | undefined;
|
||||
this.forEachEnumCase(enumType, "none", theName => {
|
||||
name = name || theName;
|
||||
});
|
||||
if (name !== undefined) {
|
||||
return [exp, " == ", this.nameForNamedType(enumType), "::", name];
|
||||
}
|
||||
return exp;
|
||||
},
|
||||
unionType => {
|
||||
const nullable = nullableFromUnion(unionType);
|
||||
if (nullable !== null) {
|
||||
if (["class", "map", "array"].indexOf(nullable.kind) >= 0) {
|
||||
return this.exampleUse(nullable, exp, depth, true);
|
||||
}
|
||||
return [exp, ".nil?"];
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private jsonSample(t: Type): Sourcelike {
|
||||
function inner() {
|
||||
if (t instanceof ArrayType) {
|
||||
return "[…]";
|
||||
} else if (t instanceof MapType) {
|
||||
return "{…}";
|
||||
} else if (t instanceof ClassType) {
|
||||
return "{…}";
|
||||
} else {
|
||||
return "…";
|
||||
}
|
||||
}
|
||||
return `"${inner()}"`;
|
||||
}
|
||||
|
||||
private fromDynamic(
|
||||
t: Type,
|
||||
e: Sourcelike,
|
||||
optional: boolean = false,
|
||||
castPrimitives: boolean = false
|
||||
): Sourcelike {
|
||||
const primitiveCast = [this.dryType(t, optional), "[", e, "]"];
|
||||
const primitive = castPrimitives ? primitiveCast : e;
|
||||
return matchType<Sourcelike>(
|
||||
t,
|
||||
_anyType => primitive,
|
||||
_nullType => primitive,
|
||||
_boolType => primitive,
|
||||
_integerType => primitive,
|
||||
_doubleType => primitive,
|
||||
_stringType => primitive,
|
||||
arrayType => [
|
||||
e,
|
||||
optional ? "&" : "",
|
||||
".map { |x| ",
|
||||
this.fromDynamic(arrayType.items, "x", false, true),
|
||||
" }"
|
||||
],
|
||||
classType => {
|
||||
const expression = [this.nameForNamedType(classType), ".from_dynamic!(", e, ")"];
|
||||
return optional ? [e, " ? ", expression, " : nil"] : expression;
|
||||
},
|
||||
mapType => [
|
||||
e,
|
||||
optional ? "&" : "",
|
||||
".map { |k, v| [k, ",
|
||||
this.fromDynamic(mapType.values, "v", false, true),
|
||||
"] }.to_h"
|
||||
],
|
||||
enumType => {
|
||||
const expression = ["Types::", this.nameForNamedType(enumType), "[", e, "]"];
|
||||
return optional ? [e, ".nil? ? nil : ", expression] : expression;
|
||||
},
|
||||
unionType => {
|
||||
const nullable = nullableFromUnion(unionType);
|
||||
if (nullable !== null) {
|
||||
return [e, ".nil? ? nil : ", this.fromDynamic(nullable, e)];
|
||||
}
|
||||
const expression = [this.nameForNamedType(unionType), ".from_dynamic!(", e, ")"];
|
||||
return optional ? [e, " ? ", expression, " : nil"] : expression;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private toDynamic(t: Type, e: Sourcelike, optional: boolean = false): Sourcelike {
|
||||
if (this.marshalsImplicitlyToDynamic(t)) {
|
||||
return e;
|
||||
}
|
||||
return matchType<Sourcelike>(
|
||||
t,
|
||||
_anyType => e,
|
||||
_nullType => e,
|
||||
_boolType => e,
|
||||
_integerType => e,
|
||||
_doubleType => e,
|
||||
_stringType => e,
|
||||
arrayType => [e, optional ? "&" : "", ".map { |x| ", this.toDynamic(arrayType.items, "x"), " }"],
|
||||
_classType => [e, optional ? "&" : "", ".to_dynamic"],
|
||||
mapType => [e, optional ? "&" : "", ".map { |k, v| [k, ", this.toDynamic(mapType.values, "v"), "] }.to_h"],
|
||||
_enumType => e,
|
||||
unionType => {
|
||||
const nullable = nullableFromUnion(unionType);
|
||||
if (nullable !== null) {
|
||||
return this.toDynamic(nullable, e, true);
|
||||
}
|
||||
if (this.marshalsImplicitlyToDynamic(unionType)) {
|
||||
return e;
|
||||
}
|
||||
return [e, optional ? "&" : "", ".to_dynamic"];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private marshalsImplicitlyToDynamic(t: Type): boolean {
|
||||
return matchType<boolean>(
|
||||
t,
|
||||
_anyType => true,
|
||||
_nullType => true,
|
||||
_boolType => true,
|
||||
_integerType => true,
|
||||
_doubleType => true,
|
||||
_stringType => true,
|
||||
arrayType => this.marshalsImplicitlyToDynamic(arrayType.items),
|
||||
_classType => false,
|
||||
mapType => this.marshalsImplicitlyToDynamic(mapType.values),
|
||||
_enumType => true,
|
||||
unionType => {
|
||||
const nullable = nullableFromUnion(unionType);
|
||||
if (nullable !== null) {
|
||||
return this.marshalsImplicitlyToDynamic(nullable);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private emitBlock(source: Sourcelike, emit: () => void) {
|
||||
this.emitLine(source);
|
||||
this.indent(emit);
|
||||
this.emitLine("end");
|
||||
}
|
||||
|
||||
private emitClass(c: ClassType, className: Name) {
|
||||
this.emitDescription(this.descriptionForType(c));
|
||||
this.emitBlock(["class ", className, " < Dry::Struct"], () => {
|
||||
let table: Sourcelike[][] = [];
|
||||
let count = c.properties.count();
|
||||
this.forEachClassProperty(c, "none", (name, jsonName, p) => {
|
||||
const last = --count === 0;
|
||||
const description = this.descriptionForClassProperty(c, jsonName);
|
||||
const attribute = [
|
||||
["attribute :", name, ","],
|
||||
[" ", this.dryType(p.type), p.isOptional ? ".optional" : ""]
|
||||
];
|
||||
if (description !== undefined) {
|
||||
if (table.length > 0) {
|
||||
this.emitTable(table);
|
||||
table = [];
|
||||
}
|
||||
this.ensureBlankLine();
|
||||
this.emitDescriptionBlock(description);
|
||||
this.emitLine(attribute);
|
||||
if (!last) {
|
||||
this.ensureBlankLine();
|
||||
}
|
||||
} else {
|
||||
table.push(attribute);
|
||||
}
|
||||
});
|
||||
if (table.length > 0) {
|
||||
this.emitTable(table);
|
||||
}
|
||||
|
||||
if (this._justTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureBlankLine();
|
||||
this.emitBlock(["def self.from_dynamic!(d)"], () => {
|
||||
this.emitLine(`raise ArgumentError, "Argument is not a hash" unless d.is_a? Hash`);
|
||||
this.emitLine("new(");
|
||||
this.indent(() => {
|
||||
const inits: Sourcelike[][] = [];
|
||||
this.forEachClassProperty(c, "none", (name, jsonName, p) => {
|
||||
const dynamic = p.isOptional
|
||||
? // If key is not found in hash, this will be nil
|
||||
`d["${stringEscape(jsonName)}"]`
|
||||
: // This will raise a runtime error if the key is not found in the hash
|
||||
`d.fetch("${stringEscape(jsonName)}")`;
|
||||
|
||||
const expression = this.fromDynamic(p.type, dynamic, p.isOptional);
|
||||
inits.push([[name, ": "], [expression, ","]]);
|
||||
});
|
||||
this.emitTable(inits);
|
||||
});
|
||||
this.emitLine(")");
|
||||
});
|
||||
|
||||
this.ensureBlankLine();
|
||||
this.emitBlock("def self.from_json!(json)", () => {
|
||||
this.emitLine("from_dynamic!(JSON.parse(json))");
|
||||
});
|
||||
|
||||
this.ensureBlankLine();
|
||||
this.emitBlock(["def to_dynamic"], () => {
|
||||
this.emitLine("{");
|
||||
this.indent(() => {
|
||||
const inits: Sourcelike[][] = [];
|
||||
this.forEachClassProperty(c, "none", (name, jsonName, p) => {
|
||||
const expression = this.toDynamic(p.type, ["@", name], p.isOptional);
|
||||
inits.push([[`"${stringEscape(jsonName)}"`], [" => ", expression, ","]]);
|
||||
});
|
||||
this.emitTable(inits);
|
||||
});
|
||||
this.emitLine("}");
|
||||
});
|
||||
this.ensureBlankLine();
|
||||
this.emitBlock("def to_json(options = nil)", () => {
|
||||
this.emitLine("JSON.generate(to_dynamic, options)");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private emitEnum(e: EnumType, enumName: Name) {
|
||||
this.emitDescription(this.descriptionForType(e));
|
||||
this.emitBlock(["module ", enumName], () => {
|
||||
const table: Sourcelike[][] = [];
|
||||
this.forEachEnumCase(e, "none", (name, json) => {
|
||||
table.push([[name], [` = "${stringEscape(json)}"`]]);
|
||||
});
|
||||
this.emitTable(table);
|
||||
});
|
||||
}
|
||||
|
||||
private emitUnion(u: UnionType, unionName: Name) {
|
||||
this.emitDescription(this.descriptionForType(u));
|
||||
this.emitBlock(["class ", unionName, " < Dry::Struct"], () => {
|
||||
const table: Sourcelike[][] = [];
|
||||
this.forEachUnionMember(u, u.children, "none", null, (name, t) => {
|
||||
table.push([["attribute :", name, ", "], [this.dryType(t, true)]]);
|
||||
});
|
||||
this.emitTable(table);
|
||||
|
||||
if (this._justTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureBlankLine();
|
||||
const [maybeNull, nonNulls] = removeNullFromUnion(u, false);
|
||||
this.emitBlock("def self.from_dynamic!(d)", () => {
|
||||
const memberNames = u.children.map((member: Type) => this.nameForUnionMember(u, member));
|
||||
this.forEachUnionMember(u, u.children, "none", null, (name, t) => {
|
||||
const nilMembers = memberNames
|
||||
.remove(name)
|
||||
.toArray()
|
||||
.map((memberName: Name) => [", ", memberName, ": nil"]);
|
||||
if (this.marshalsImplicitlyToDynamic(t)) {
|
||||
this.emitBlock(["if schema[:", name, "].right.valid? d"], () => {
|
||||
this.emitLine("return new(", name, ": d", nilMembers, ")");
|
||||
});
|
||||
} else {
|
||||
this.emitLine("begin");
|
||||
this.indent(() => {
|
||||
this.emitLine("value = ", this.fromDynamic(t, "d"));
|
||||
this.emitBlock(["if schema[:", name, "].right.valid? value"], () => {
|
||||
this.emitLine("return new(", name, ": value", nilMembers, ")");
|
||||
});
|
||||
});
|
||||
this.emitLine("rescue");
|
||||
this.emitLine("end");
|
||||
}
|
||||
});
|
||||
this.emitLine(`raise "Invalid union"`);
|
||||
});
|
||||
|
||||
this.ensureBlankLine();
|
||||
this.emitBlock("def self.from_json!(json)", () => {
|
||||
this.emitLine("from_dynamic!(JSON.parse(json))");
|
||||
});
|
||||
|
||||
this.ensureBlankLine();
|
||||
this.emitBlock("def to_dynamic", () => {
|
||||
let first = true;
|
||||
this.forEachUnionMember(u, nonNulls, "none", null, (name, t) => {
|
||||
this.emitLine(first ? "if" : "elsif", " @", name, " != nil");
|
||||
this.indent(() => {
|
||||
this.emitLine(this.toDynamic(t, ["@", name]));
|
||||
});
|
||||
first = false;
|
||||
});
|
||||
if (maybeNull !== null) {
|
||||
this.emitLine("else");
|
||||
this.indent(() => {
|
||||
this.emitLine("nil");
|
||||
});
|
||||
}
|
||||
this.emitLine("end");
|
||||
});
|
||||
|
||||
this.ensureBlankLine();
|
||||
this.emitBlock("def to_json(options = nil)", () => {
|
||||
this.emitLine("JSON.generate(to_dynamic, options)");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private emitEnumDeclaration(e: EnumType, name: Name) {
|
||||
const cases: Sourcelike[][] = [];
|
||||
this.forEachEnumCase(e, "none", (_name, json) => {
|
||||
cases.push([cases.length === 0 ? "" : ", ", `"${stringEscape(json)}"`]);
|
||||
});
|
||||
this.emitLine(name, " = Types::Strict::String.enum(", ...cases, ")");
|
||||
}
|
||||
|
||||
protected emitSourceStructure() {
|
||||
if (this.leadingComments !== undefined) {
|
||||
this.emitCommentLines(this.leadingComments);
|
||||
} else if (!this._justTypes) {
|
||||
this.emitLine("# This code may look unusually verbose for Ruby (and it is), but");
|
||||
this.emitLine("# it performs some subtle and complex validation of JSON data.");
|
||||
this.emitLine("#");
|
||||
this.emitLine("# To parse this JSON, add 'dry-struct' and 'dry-types' gems, then do:");
|
||||
this.emitLine("#");
|
||||
this.forEachTopLevel("none", (topLevel, name) => {
|
||||
const variable = modifySource(snakeCase, name);
|
||||
this.emitLine("# ", variable, " = ", name, ".from_json! ", this.jsonSample(topLevel));
|
||||
this.emitLine("# puts ", this.exampleUse(topLevel, variable));
|
||||
this.emitLine("#");
|
||||
});
|
||||
this.emitLine("# If from_json! succeeds, the value returned matches the schema.");
|
||||
}
|
||||
this.ensureBlankLine();
|
||||
|
||||
this.emitLine("require 'json'");
|
||||
this.emitLine("require 'dry-types'");
|
||||
this.emitLine("require 'dry-struct'");
|
||||
this.ensureBlankLine();
|
||||
|
||||
this.emitBlock(["module Types"], () => {
|
||||
this.emitLine("include Dry::Types.module");
|
||||
this.emitLine("StrictDouble = Strict::Int | Strict::Float");
|
||||
this.forEachNamedType(
|
||||
"none",
|
||||
(_c, _n) => undefined,
|
||||
(e, n) => this.emitEnumDeclaration(e, n),
|
||||
(_u, _n) => undefined
|
||||
);
|
||||
});
|
||||
|
||||
this.forEachDeclaration("leading-and-interposing", decl => {
|
||||
if (decl.kind === "forward") {
|
||||
this.emitCommentLines(["(forward declaration)"]);
|
||||
this.emitLine("class ", this.nameForNamedType(decl.type), " < Dry::Struct; end");
|
||||
}
|
||||
});
|
||||
|
||||
this.forEachNamedType(
|
||||
"leading-and-interposing",
|
||||
(c, n) => this.emitClass(c, n),
|
||||
(e, n) => this.emitEnum(e, n),
|
||||
(u, n) => this.emitUnion(u, n)
|
||||
);
|
||||
|
||||
if (!this._justTypes) {
|
||||
this.forEachTopLevel(
|
||||
"leading-and-interposing",
|
||||
(topLevel, name) => {
|
||||
const self = modifySource(snakeCase, name);
|
||||
|
||||
// The json gem defines to_json on maps and primitives, so we only need to supply
|
||||
// it for arrays.
|
||||
const needsToJsonDefined = "array" === topLevel.kind;
|
||||
|
||||
this.emitBlock(["class ", name], () => {
|
||||
this.emitBlock(["def self.from_json!(json)"], () => {
|
||||
if (needsToJsonDefined) {
|
||||
this.emitLine(
|
||||
self,
|
||||
" = ",
|
||||
this.fromDynamic(topLevel, "JSON.parse(json, quirks_mode: true)")
|
||||
);
|
||||
this.emitBlock([self, ".define_singleton_method(:to_json) do"], () => {
|
||||
this.emitLine("JSON.generate(", this.toDynamic(topLevel, "self"), ")");
|
||||
});
|
||||
this.emitLine(self);
|
||||
} else {
|
||||
this.emitLine(this.fromDynamic(topLevel, "JSON.parse(json, quirks_mode: true)"));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
t => this.namedTypeToNameForTopLevel(t) === undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
export const keywords = [
|
||||
"__ENCODING__",
|
||||
"__FILE__",
|
||||
"__LINE__",
|
||||
"alias",
|
||||
"and",
|
||||
"begin",
|
||||
"BEGIN",
|
||||
"break",
|
||||
"case",
|
||||
"class",
|
||||
"def",
|
||||
"defined?",
|
||||
"do",
|
||||
"else",
|
||||
"elsif",
|
||||
"end",
|
||||
"END",
|
||||
"ensure",
|
||||
"false",
|
||||
"for",
|
||||
"if",
|
||||
"in",
|
||||
"module",
|
||||
"next",
|
||||
"nil",
|
||||
"not",
|
||||
"or",
|
||||
"redo",
|
||||
"rescue",
|
||||
"retry",
|
||||
"return",
|
||||
"self",
|
||||
"super",
|
||||
"then",
|
||||
"true",
|
||||
"undef",
|
||||
"unless",
|
||||
"until",
|
||||
"when",
|
||||
"while",
|
||||
"yield"
|
||||
];
|
||||
|
||||
const globalClasses = [
|
||||
"ArgumentError",
|
||||
"Array",
|
||||
"BasicObject",
|
||||
"Class",
|
||||
"ClosedQueueError",
|
||||
"Comparable",
|
||||
"Complex",
|
||||
"ConditionVariable",
|
||||
"Continuation",
|
||||
"Data",
|
||||
"Date",
|
||||
"Dir",
|
||||
"ENV",
|
||||
"EOFError",
|
||||
"Encoding",
|
||||
"EncodingError",
|
||||
"Enumerable",
|
||||
"Enumerator",
|
||||
"Errno",
|
||||
"Exception",
|
||||
"FalseClass",
|
||||
"Fiber",
|
||||
"FiberError",
|
||||
"File",
|
||||
"FileTest",
|
||||
"Float",
|
||||
"FloatDomainError",
|
||||
"FrozenError",
|
||||
"GC",
|
||||
"Hash",
|
||||
"IO",
|
||||
"IOError",
|
||||
"IndexError",
|
||||
"Integer",
|
||||
"Interrupt",
|
||||
"KeyError",
|
||||
"LoadError",
|
||||
"LocalJumpError",
|
||||
"Marshal",
|
||||
"MatchData",
|
||||
"Math",
|
||||
"Method",
|
||||
"Module",
|
||||
"Mutex",
|
||||
"NameError",
|
||||
"NilClass",
|
||||
"NoMemoryError",
|
||||
"NoMethodError",
|
||||
"NotImplementedError",
|
||||
"Numeric",
|
||||
"Object",
|
||||
"ObjectSpace",
|
||||
"Proc",
|
||||
"Process",
|
||||
"Queue",
|
||||
"Random",
|
||||
"Range",
|
||||
"RangeError",
|
||||
"Rational",
|
||||
"Regexp",
|
||||
"RegexpError",
|
||||
"RubyVM",
|
||||
"RuntimeError",
|
||||
"ScriptError",
|
||||
"SecurityError",
|
||||
"Set",
|
||||
"Signal",
|
||||
"SignalException",
|
||||
"SizedQueue",
|
||||
"StandardError",
|
||||
"StopIteration",
|
||||
"String",
|
||||
"Struct",
|
||||
"Symbol",
|
||||
"SyntaxError",
|
||||
"SystemCallError",
|
||||
"SystemExit",
|
||||
"SystemStackError",
|
||||
"Thread",
|
||||
"ThreadError",
|
||||
"ThreadGroup",
|
||||
"Time",
|
||||
"TracePoint",
|
||||
"TrueClass",
|
||||
"TypeError",
|
||||
"UnboundMethod",
|
||||
"UncaughtThrowError",
|
||||
"Undefined",
|
||||
"UnicodeNormalize",
|
||||
"Warning",
|
||||
"ZeroDivisionError"
|
||||
];
|
||||
|
||||
const kernel = [
|
||||
"__callee__",
|
||||
"__dir__",
|
||||
"__id__",
|
||||
"__method__",
|
||||
"__send__",
|
||||
"!",
|
||||
"!=",
|
||||
"!~",
|
||||
"<",
|
||||
"<=",
|
||||
"<=>",
|
||||
"==",
|
||||
"===",
|
||||
"=~",
|
||||
">",
|
||||
">=",
|
||||
"abort",
|
||||
"ancestors",
|
||||
"Array",
|
||||
"at_exit",
|
||||
"autoload",
|
||||
"autoload?",
|
||||
"binding",
|
||||
"block_given?",
|
||||
"caller",
|
||||
"caller_locations",
|
||||
"catch",
|
||||
"class",
|
||||
"class_eval",
|
||||
"class_exec",
|
||||
"class_variable_defined?",
|
||||
"class_variable_get",
|
||||
"class_variable_set",
|
||||
"class_variables",
|
||||
"clone",
|
||||
"Complex",
|
||||
"const_defined?",
|
||||
"const_get",
|
||||
"const_missing",
|
||||
"const_set",
|
||||
"constants",
|
||||
"define_singleton_method",
|
||||
"deprecate_constant",
|
||||
"display",
|
||||
"dup",
|
||||
"enum_for",
|
||||
"eql?",
|
||||
"equal?",
|
||||
"eval",
|
||||
"exec",
|
||||
"exit",
|
||||
"exit!",
|
||||
"extend",
|
||||
"fail",
|
||||
"Float",
|
||||
"fork",
|
||||
"format",
|
||||
"freeze",
|
||||
"frozen?",
|
||||
"gets",
|
||||
"global_variables",
|
||||
"hash",
|
||||
"Hash",
|
||||
"include",
|
||||
"include?",
|
||||
"included_modules",
|
||||
"inspect",
|
||||
"instance_eval",
|
||||
"instance_exec",
|
||||
"instance_method",
|
||||
"instance_methods",
|
||||
"instance_of?",
|
||||
"instance_variable_defined?",
|
||||
"instance_variable_get",
|
||||
"instance_variable_set",
|
||||
"instance_variables",
|
||||
"Integer",
|
||||
"is_a?",
|
||||
"iterator?",
|
||||
"itself",
|
||||
"kind_of?",
|
||||
"lambda",
|
||||
"load",
|
||||
"local_variables",
|
||||
"loop",
|
||||
"method",
|
||||
"method_defined?",
|
||||
"methods",
|
||||
"module_eval",
|
||||
"module_exec",
|
||||
"name",
|
||||
"nil?",
|
||||
"object_id",
|
||||
"open",
|
||||
"p",
|
||||
"prepend",
|
||||
"print",
|
||||
"printf",
|
||||
"private_class_method",
|
||||
"private_constant",
|
||||
"private_instance_methods",
|
||||
"private_method_defined?",
|
||||
"private_methods",
|
||||
"proc",
|
||||
"protected_instance_methods",
|
||||
"protected_method_defined?",
|
||||
"protected_methods",
|
||||
"public_class_method",
|
||||
"public_constant",
|
||||
"public_instance_method",
|
||||
"public_instance_methods",
|
||||
"public_method",
|
||||
"public_method_defined?",
|
||||
"public_methods",
|
||||
"public_send",
|
||||
"putc",
|
||||
"puts",
|
||||
"raise",
|
||||
"rand",
|
||||
"Rational",
|
||||
"readline",
|
||||
"readlines",
|
||||
"remove_class_variable",
|
||||
"remove_instance_variable",
|
||||
"require",
|
||||
"require_relative",
|
||||
"respond_to?",
|
||||
"select",
|
||||
"send",
|
||||
"set_trace_func",
|
||||
"singleton_class",
|
||||
"singleton_class?",
|
||||
"singleton_method",
|
||||
"singleton_methods",
|
||||
"sleep",
|
||||
"spawn",
|
||||
"sprintf",
|
||||
"srand",
|
||||
"String",
|
||||
"syscall",
|
||||
"system",
|
||||
"taint",
|
||||
"tainted?",
|
||||
"tap",
|
||||
"test",
|
||||
"throw",
|
||||
"to_enum",
|
||||
"to_s",
|
||||
"trace_var",
|
||||
"trap",
|
||||
"trust",
|
||||
"untaint",
|
||||
"untrace_var",
|
||||
"untrust",
|
||||
"untrusted?",
|
||||
"warn"
|
||||
];
|
||||
|
||||
export const globals = kernel.concat(globalClasses);
|
||||
|
||||
export const reservedProperties = [
|
||||
"__id__",
|
||||
"__send__",
|
||||
"call",
|
||||
"class",
|
||||
"clone",
|
||||
"constrained_type",
|
||||
"constrained?",
|
||||
"constrained",
|
||||
"constructor",
|
||||
"default",
|
||||
"define_singleton_method",
|
||||
"display",
|
||||
"dup",
|
||||
"enum_for",
|
||||
"enum",
|
||||
"extend",
|
||||
"freeze",
|
||||
"gem",
|
||||
"hash",
|
||||
"inspect",
|
||||
"instance_eval",
|
||||
"instance_exec",
|
||||
"instance_variable_defined?",
|
||||
"instance_variable_get",
|
||||
"instance_variable_set",
|
||||
"instance_variables",
|
||||
"itself",
|
||||
"meta",
|
||||
"method",
|
||||
"methods",
|
||||
"object_id",
|
||||
"optional",
|
||||
"options",
|
||||
"pristine",
|
||||
"private_methods",
|
||||
"protected_methods",
|
||||
"public_method",
|
||||
"public_methods",
|
||||
"public_send",
|
||||
"remove_instance_variable",
|
||||
"rule",
|
||||
"safe",
|
||||
"send",
|
||||
"singleton_class",
|
||||
"singleton_method",
|
||||
"singleton_methods",
|
||||
"taint",
|
||||
"tap",
|
||||
"to_ast",
|
||||
"to_enum",
|
||||
"to_json",
|
||||
"to_s",
|
||||
"trust",
|
||||
"try",
|
||||
"type",
|
||||
"untaint",
|
||||
"untrust",
|
||||
"with"
|
||||
];
|
|
@ -473,6 +473,7 @@ export const allFixtures: Fixture[] = [
|
|||
new JSONFixture(languages.GoLanguage),
|
||||
new JSONFixture(languages.CPlusPlusLanguage),
|
||||
new JSONFixture(languages.RustLanguage),
|
||||
new JSONFixture(languages.RubyLanguage),
|
||||
new JSONFixture(languages.ElmLanguage),
|
||||
new JSONFixture(languages.SwiftLanguage),
|
||||
new JSONFixture(languages.ObjectiveCLanguage),
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
BUNDLE_PATH: "vendor/bundle"
|
|
@ -0,0 +1 @@
|
|||
vendor
|
|
@ -0,0 +1,4 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'dry-types'
|
||||
gem 'dry-struct'
|
|
@ -0,0 +1,41 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
concurrent-ruby (1.0.5)
|
||||
dry-configurable (0.7.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-container (0.6.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-configurable (~> 0.1, >= 0.1.3)
|
||||
dry-core (0.4.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-equalizer (0.2.0)
|
||||
dry-logic (0.4.2)
|
||||
dry-container (~> 0.2, >= 0.2.6)
|
||||
dry-core (~> 0.2)
|
||||
dry-equalizer (~> 0.2)
|
||||
dry-struct (0.4.0)
|
||||
dry-core (~> 0.4, >= 0.4.1)
|
||||
dry-equalizer (~> 0.2)
|
||||
dry-types (~> 0.12, >= 0.12.2)
|
||||
ice_nine (~> 0.11)
|
||||
dry-types (0.12.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-configurable (~> 0.1)
|
||||
dry-container (~> 0.3)
|
||||
dry-core (~> 0.2, >= 0.2.1)
|
||||
dry-equalizer (~> 0.2)
|
||||
dry-logic (~> 0.4, >= 0.4.2)
|
||||
inflecto (~> 0.0.0, >= 0.0.2)
|
||||
ice_nine (0.11.2)
|
||||
inflecto (0.0.2)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
dry-struct
|
||||
dry-types
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.1
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require 'json'
|
||||
require './TopLevel.rb'
|
||||
|
||||
json = File.read(ARGV[0])
|
||||
top = TopLevel.from_json! json
|
||||
|
||||
puts top.to_json
|
|
@ -39,11 +39,7 @@ export const CSharpLanguage: Language = {
|
|||
skipMiscJSON: false,
|
||||
skipSchema: [],
|
||||
rendererOptions: {},
|
||||
quickTestRendererOptions: [
|
||||
{ "array-type": "list" },
|
||||
{ "csharp-version": "5" },
|
||||
{ density: "dense" }
|
||||
],
|
||||
quickTestRendererOptions: [{ "array-type": "list" }, { "csharp-version": "5" }, { density: "dense" }],
|
||||
sourceFiles: ["src/Language/CSharp.ts"]
|
||||
};
|
||||
|
||||
|
@ -60,11 +56,7 @@ export const JavaLanguage: Language = {
|
|||
allowMissingNull: false,
|
||||
output: "src/main/java/io/quicktype/TopLevel.java",
|
||||
topLevel: "TopLevel",
|
||||
skipJSON: [
|
||||
"identifiers.json",
|
||||
"simple-identifiers.json",
|
||||
"nst-test-suite.json"
|
||||
],
|
||||
skipJSON: ["identifiers.json", "simple-identifiers.json", "nst-test-suite.json"],
|
||||
skipMiscJSON: false,
|
||||
skipSchema: ["keyword-unions.schema"], // generates classes with names that are case-insensitively equal
|
||||
rendererOptions: {},
|
||||
|
@ -98,6 +90,26 @@ export const RustLanguage: Language = {
|
|||
sourceFiles: ["src/Language/Rust.ts"]
|
||||
};
|
||||
|
||||
export const RubyLanguage: Language = {
|
||||
name: "ruby",
|
||||
base: "test/fixtures/ruby",
|
||||
setupCommand: "bundle install --path vendor/bundle",
|
||||
compileCommand: "true",
|
||||
runCommand(sample: string) {
|
||||
return `bundle exec main.rb "${sample}"`;
|
||||
},
|
||||
diffViaSchema: true,
|
||||
allowMissingNull: true,
|
||||
output: "TopLevel.rb",
|
||||
topLevel: "TopLevel",
|
||||
skipJSON: [],
|
||||
skipSchema: [],
|
||||
skipMiscJSON: false,
|
||||
rendererOptions: {},
|
||||
quickTestRendererOptions: [],
|
||||
sourceFiles: ["src/Language/Ruby/index.ts"]
|
||||
};
|
||||
|
||||
export const GoLanguage: Language = {
|
||||
name: "golang",
|
||||
base: "test/fixtures/golang",
|
||||
|
@ -108,12 +120,7 @@ export const GoLanguage: Language = {
|
|||
allowMissingNull: false,
|
||||
output: "quicktype.go",
|
||||
topLevel: "TopLevel",
|
||||
skipJSON: [
|
||||
"identifiers.json",
|
||||
"simple-identifiers.json",
|
||||
"blns-object.json",
|
||||
"nst-test-suite.json"
|
||||
],
|
||||
skipJSON: ["identifiers.json", "simple-identifiers.json", "blns-object.json", "nst-test-suite.json"],
|
||||
skipMiscJSON: false,
|
||||
skipSchema: [],
|
||||
rendererOptions: {},
|
||||
|
@ -149,9 +156,10 @@ export const ElmLanguage: Language = {
|
|||
name: "elm",
|
||||
base: "test/fixtures/elm",
|
||||
setupCommand: "rm -rf elm-stuff/build-artifacts && elm-make --yes",
|
||||
compileCommand: process.env.CI === "true"
|
||||
? "sysconfcpus -n 1 elm-make Main.elm QuickType.elm --output elm.js"
|
||||
: "elm-make Main.elm QuickType.elm --output elm.js",
|
||||
compileCommand:
|
||||
process.env.CI === "true"
|
||||
? "sysconfcpus -n 1 elm-make Main.elm QuickType.elm --output elm.js"
|
||||
: "elm-make Main.elm QuickType.elm --output elm.js",
|
||||
runCommand(sample: string) {
|
||||
return `node ./runner.js "${sample}"`;
|
||||
},
|
||||
|
@ -206,11 +214,7 @@ export const SwiftLanguage: Language = {
|
|||
skipMiscJSON: false,
|
||||
skipSchema: [],
|
||||
rendererOptions: {},
|
||||
quickTestRendererOptions: [
|
||||
{ "struct-or-class": "class" },
|
||||
{ density: "dense" },
|
||||
{ density: "normal" }
|
||||
],
|
||||
quickTestRendererOptions: [{ "struct-or-class": "class" }, { density: "dense" }, { density: "normal" }],
|
||||
sourceFiles: ["src/Language/Swift.ts"]
|
||||
};
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче