Merge pull request #708 from quicktype/csharp-optional

Csharp optional
This commit is contained in:
Mark Probst 2018-04-02 09:13:05 -07:00 коммит произвёл GitHub
Родитель 8028a5436e 187e7b5e10
Коммит 2d5397a9f5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 203 добавлений и 104 удалений

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

@ -131,7 +131,9 @@ interface Selection {
}
function expandSelectionSet(selectionSet: SelectionSetNode, inType: GQLType, optional: boolean): Selection[] {
return selectionSet.selections.reverse().map(s => ({ selection: s, inType, optional }));
return selectionSet.selections
.reverse()
.map(s => ({ selection: s, inType, optional: optional || hasOptionalDirectives(s.directives) }));
}
interface GQLSchema {
@ -275,10 +277,7 @@ class GQLQuery {
const givenName = selection.alias ? selection.alias.value : fieldName;
const field = getField(inType, fieldName);
let fieldType = this.makeIRTypeFromFieldNode(builder, selection, field.type, nameOrOverride);
if (optional) {
fieldType = makeNullable(builder, fieldType, givenName, null, nameOrOverride);
}
properties = properties.set(givenName, new ClassProperty(fieldType, false));
properties = properties.set(givenName, new ClassProperty(fieldType, optional));
break;
case "FragmentSpread": {
const fragment = this.getFragment(selection.name.value);
@ -428,8 +427,9 @@ export function makeGraphQLQueryTypes(
),
OrderedMap({ message: new ClassProperty(builder.getStringType(undefined, undefined), false) })
);
const optionalErrorArray = builder.makeNullable(
builder.getArrayType(errorType),
const errorArray = builder.getArrayType(errorType);
builder.addAttributes(
errorArray,
namesTypeAttributeKind.makeAttributes(
new TypeNames(OrderedSet(["errors"]), OrderedSet(["graphQLErrors"]), false)
)
@ -438,7 +438,7 @@ export function makeGraphQLQueryTypes(
makeNamesTypeAttributes(queryName, false),
OrderedMap({
data: new ClassProperty(dataType, false),
errors: new ClassProperty(optionalErrorArray, false)
errors: new ClassProperty(errorArray, true)
})
);
types = types.set(queryName, t);

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

@ -12,7 +12,8 @@ import {
matchType,
nullableFromUnion,
removeNullFromUnion,
directlyReachableSingleNamedType
directlyReachableSingleNamedType,
ClassProperty
} from "../Type";
import { TypeGraph } from "../TypeGraph";
import { Sourcelike, maybeAnnotated, modifySource } from "../Source";
@ -28,7 +29,7 @@ import { intercalate, defined, assert, panic, StringMap } from "../Support";
import { Name, DependencyName, Namer, funPrefixNamer } from "../Naming";
import { ConvenienceRenderer, ForbiddenWordsInfo } from "../ConvenienceRenderer";
import { TargetLanguage } from "../TargetLanguage";
import { StringOption, EnumOption, Option } from "../RendererOptions";
import { StringOption, EnumOption, Option, BooleanOption } from "../RendererOptions";
import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation";
import { StringTypeMapping } from "../TypeBuilder";
@ -69,13 +70,25 @@ export default class CSharpTargetLanguage extends TargetLanguage {
"6",
"secondary"
);
private readonly _checkRequiredOption = new BooleanOption(
"check-required",
"Fail if required properties are missing",
false
);
constructor() {
super("C#", ["cs", "csharp"], "cs");
}
protected getOptions(): Option<any>[] {
return [this._namespaceOption, this._versionOption, this._denseOption, this._listOption, this._featuresOption];
return [
this._namespaceOption,
this._versionOption,
this._denseOption,
this._listOption,
this._featuresOption,
this._checkRequiredOption
];
}
protected get partialStringTypeMapping(): Partial<StringTypeMapping> {
@ -86,6 +99,10 @@ export default class CSharpTargetLanguage extends TargetLanguage {
return true;
}
get supportsOptionalClassProperties(): boolean {
return true;
}
protected get rendererClass(): new (
targetLanguage: TargetLanguage,
graph: TypeGraph,
@ -100,6 +117,8 @@ const namingFunction = funPrefixNamer("namer", csNameStyle);
// FIXME: Make a Named?
const denseJsonPropertyName = "J";
const denseRequiredEnumName = "R";
const denseNullValueHandlingEnumName = "N";
function isStartCharacter(utf16Unit: number): boolean {
if (unicode.isAlphabetic(utf16Unit)) {
@ -133,13 +152,13 @@ function csNameStyle(original: string): string {
}
function isValueType(t: Type): boolean {
if (t instanceof UnionType) {
return nullableFromUnion(t) === null;
}
return ["integer", "double", "bool", "enum", "date-time"].indexOf(t.kind) >= 0;
}
export class CSharpRenderer extends ConvenienceRenderer {
protected readonly needHelpers: boolean;
protected readonly needAttributes: boolean;
constructor(
targetLanguage: TargetLanguage,
graph: TypeGraph,
@ -147,12 +166,9 @@ export class CSharpRenderer extends ConvenienceRenderer {
protected readonly namespaceName: string,
private readonly _version: Version,
protected readonly dense: boolean,
private readonly _useList: boolean,
outputFeatures: OutputFeatures
private readonly _useList: boolean
) {
super(targetLanguage, graph, leadingComments);
this.needHelpers = outputFeatures.helpers;
this.needAttributes = outputFeatures.attributes;
}
protected forbiddenNamesForGlobalNamespace(): string[] {
@ -243,8 +259,8 @@ export class CSharpRenderer extends ConvenienceRenderer {
);
}
protected nullableCSType(t: Type): Sourcelike {
const csType = this.csType(t);
protected nullableCSType(t: Type, withIssues: boolean = false): Sourcelike {
const csType = this.csType(t, withIssues);
if (isValueType(t)) {
return [csType, "?"];
} else {
@ -281,7 +297,7 @@ export class CSharpRenderer extends ConvenienceRenderer {
this.emitBlock(emitter);
}
protected attributeForProperty(_jsonName: string): Sourcelike | undefined {
protected attributesForProperty(_property: ClassProperty, _jsonName: string): Sourcelike[] | undefined {
return undefined;
}
@ -294,6 +310,10 @@ export class CSharpRenderer extends ConvenienceRenderer {
}
}
protected blankLinesBetweenAttributes(): boolean {
return false;
}
private emitClassDefinition(c: ClassType, className: Name): void {
this.emitType(
this.descriptionForType(c),
@ -303,16 +323,16 @@ export class CSharpRenderer extends ConvenienceRenderer {
this.superclassForType(c),
() => {
if (c.properties.isEmpty()) return;
const blankLines = this.needAttributes && !this.dense ? "interposing" : "none";
const blankLines = this.blankLinesBetweenAttributes() ? "interposing" : "none";
let columns: Sourcelike[][] = [];
let isFirstProperty = true;
let previousDescription: string[] | undefined = undefined;
this.forEachClassProperty(c, blankLines, (name, jsonName, p) => {
const csType = this.csType(p.type, true);
const attribute = this.attributeForProperty(jsonName);
const csType = p.isOptional ? this.nullableCSType(p.type, true) : this.csType(p.type, true);
const attributes = this.attributesForProperty(p, jsonName);
const description = this.descriptionForClassProperty(c, jsonName);
const property = ["public ", csType, " ", name, " { get; set; }"];
if (!this.needAttributes) {
if (attributes === undefined) {
if (
// Descriptions should be preceded by an empty line
(!isFirstProperty && description !== undefined) ||
@ -323,12 +343,12 @@ export class CSharpRenderer extends ConvenienceRenderer {
}
this.emitDescription(description);
this.emitLine(property);
} else if (this.dense && attribute !== undefined) {
} else if (this.dense && attributes.length > 0) {
const comment = description === undefined ? "" : ` // ${description.join("; ")}`;
columns.push([attribute, " ", property, comment]);
columns.push([attributes, " ", property, comment]);
} else {
this.emitDescription(description);
if (attribute !== undefined) {
for (const attribute of attributes) {
this.emitLine(attribute);
}
this.emitLine(property);
@ -410,7 +430,7 @@ export class CSharpRenderer extends ConvenienceRenderer {
}
protected emitUsings(): void {
for (const ns of ["System", "System.Collections.Generic", "System.Net"]) {
for (const ns of ["System", "System.Collections.Generic"]) {
this.emitUsing(ns);
}
}
@ -419,32 +439,37 @@ export class CSharpRenderer extends ConvenienceRenderer {
return;
}
private emitTypesAndSupport = (): void => {
if (this.needAttributes || this.needHelpers) {
this.emitUsings();
}
private emitTypesAndSupport(): void {
this.forEachClass("leading-and-interposing", (c, name) => this.emitClassDefinition(c, name));
this.forEachEnum("leading-and-interposing", (e, name) => this.emitEnumDefinition(e, name));
this.forEachUnion("leading-and-interposing", (u, name) => this.emitUnionDefinition(u, name));
this.emitRequiredHelpers();
};
}
protected emitDefaultLeadingComments(): void {
return;
}
protected needNamespace(): boolean {
return true;
}
protected emitSourceStructure(): void {
if (this.leadingComments !== undefined) {
this.emitCommentLines(this.leadingComments);
} else if (this.needHelpers) {
} else {
this.emitDefaultLeadingComments();
}
this.ensureBlankLine();
if (this.needHelpers || this.needAttributes) {
if (this.needNamespace()) {
this.emitLine("namespace ", this.namespaceName);
this.emitBlock(this.emitTypesAndSupport);
this.emitBlock(() => {
this.emitUsings();
this.emitTypesAndSupport();
});
} else {
this.emitUsings();
this.emitTypesAndSupport();
}
}
@ -470,26 +495,48 @@ export class CSharpRenderer extends ConvenienceRenderer {
export class NewtonsoftCSharpRenderer extends CSharpRenderer {
private _enumExtensionsNames = Map<Name, Name>();
private readonly _needHelpers: boolean;
private readonly _needAttributes: boolean;
constructor(
targetLanguage: TargetLanguage,
graph: TypeGraph,
leadingComments: string[] | undefined,
namespaceName: string,
version: Version,
dense: boolean,
useList: boolean,
outputFeatures: OutputFeatures,
private readonly _checkRequiredProperties: boolean
) {
super(targetLanguage, graph, leadingComments, namespaceName, version, dense, useList);
this._needHelpers = outputFeatures.helpers;
this._needAttributes = outputFeatures.attributes;
}
protected forbiddenNamesForGlobalNamespace(): string[] {
return super
.forbiddenNamesForGlobalNamespace()
.concat([
"Converter",
"JsonConverter",
"JsonSerializer",
"JsonWriter",
"JsonToken",
"Serialize",
"Newtonsoft",
"MetadataPropertyHandling",
"DateParseHandling",
"FromJson"
]);
const forbidden = [
"Converter",
"JsonConverter",
"JsonSerializer",
"JsonWriter",
"JsonToken",
"Serialize",
"Newtonsoft",
"MetadataPropertyHandling",
"DateParseHandling",
"FromJson",
"Required"
];
if (this.dense) {
forbidden.push("J", "R", "N");
}
return super.forbiddenNamesForGlobalNamespace().concat(forbidden);
}
protected forbiddenForClassProperties(c: ClassType, className: Name): ForbiddenWordsInfo {
const result = super.forbiddenForClassProperties(c, className);
result.names = result.names.concat(["ToJson", "FromJson"]);
result.names = result.names.concat(["ToJson", "FromJson", "Required"]);
return result;
}
@ -502,6 +549,9 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer {
}
protected emitUsings(): void {
// FIXME: We need System.Collections.Generic whenever we have maps or use List.
if (!this._needAttributes && !this._needHelpers) return;
super.emitUsings();
this.ensureBlankLine();
@ -511,10 +561,14 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer {
if (this.dense) {
this.emitUsing([denseJsonPropertyName, " = Newtonsoft.Json.JsonPropertyAttribute"]);
this.emitUsing([denseRequiredEnumName, " = Newtonsoft.Json.Required"]);
this.emitUsing([denseNullValueHandlingEnumName, " = Newtonsoft.Json.NullValueHandling"]);
}
}
protected emitDefaultLeadingComments(): void {
if (!this._needHelpers) return;
this.emitLine(
"// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do",
this.topLevels.size === 1 ? "" : " one of these",
@ -526,24 +580,41 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer {
this.forEachTopLevel("none", (t, topLevelName) => {
let rhs: Sourcelike;
if (t instanceof EnumType) {
rhs = ["JsonConvert.DeserializeObject<", topLevelName, ">(jsonString)"]
rhs = ["JsonConvert.DeserializeObject<", topLevelName, ">(jsonString)"];
} else {
rhs = [topLevelName, ".FromJson(jsonString)"];
}
this.emitLine(
"// var ",
modifySource(camelCase, topLevelName),
" = ",
rhs,
";"
);
this.emitLine("// var ", modifySource(camelCase, topLevelName), " = ", rhs, ";");
});
}
protected attributeForProperty(jsonName: string): Sourcelike {
protected attributesForProperty(property: ClassProperty, jsonName: string): Sourcelike[] | undefined {
if (!this._needAttributes) return undefined;
const t = property.type;
const jsonProperty = this.dense ? denseJsonPropertyName : "JsonProperty";
const escapedName = utf16StringEscape(jsonName);
return ["[", jsonProperty, '("', escapedName, '")]'];
const isNullable = t.isNullable;
const isOptional = property.isOptional;
const requiredClass = this.dense ? "R" : "Required";
const nullValueHandlingClass = this.dense ? "N" : "NullValueHandling";
const nullValueHandling =
isOptional && !isNullable ? [", NullValueHandling = ", nullValueHandlingClass, ".Ignore"] : [];
let required: Sourcelike;
if (!this._checkRequiredProperties || (isOptional && isNullable)) {
required = [nullValueHandling];
} else if (isOptional && !isNullable) {
required = [", Required = ", requiredClass, ".DisallowNull", nullValueHandling];
} else if (!isOptional && isNullable) {
required = [", Required = ", requiredClass, ".AllowNull"];
} else {
required = [", Required = ", requiredClass, ".Always", nullValueHandling];
}
return [["[", jsonProperty, '("', escapedName, '"', required, ")]"]];
}
protected blankLinesBetweenAttributes(): boolean {
return this._needAttributes && !this.dense;
}
private emitFromJsonForTopLevel(t: Type, name: Name): void {
@ -754,7 +825,7 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer {
const unionNames = this.namedUnions.map(this.nameForNamedType);
const allNames = enumNames.union(unionNames);
const canConvertExprs = allNames.map((n: Name): Sourcelike => ["t == typeof(", n, ")"]);
const nullableCanConvertExprs = enumNames.map((n: Name): Sourcelike => ["t == typeof(", n, "?)"]);
const nullableCanConvertExprs = allNames.map((n: Name): Sourcelike => ["t == typeof(", n, "?)"]);
const canConvertExpr = intercalate(" || ", canConvertExprs.union(nullableCanConvertExprs));
// FIXME: make Iterable<any, Sourcelike> a Sourcelike, too?
this.emitExpressionMember("public override bool CanConvert(Type t)", canConvertExpr.toArray());
@ -773,7 +844,7 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer {
this.emitLine("return ", extensionsName, ".ReadJson(reader, serializer);");
});
// FIXME: call the constructor via reflection?
this.emitTypeSwitch(unionNames, t => ["t == typeof(", t, ")"], false, false, n => {
this.emitTypeSwitch(unionNames, t => ["t == typeof(", t, ") || t == typeof(", t, "?)"], false, false, n => {
this.emitLine("return new ", n, "(reader, serializer);");
});
this.emitLine('throw new Exception("Unknown type");');
@ -818,19 +889,23 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer {
}
protected emitRequiredHelpers(): void {
if (this.needHelpers) {
if (this._needHelpers) {
this.forEachTopLevel("leading-and-interposing", (t, n) => this.emitFromJsonForTopLevel(t, n));
this.forEachEnum("leading-and-interposing", (e, n) => this.emitEnumExtension(e, n));
this.forEachUnion("leading-and-interposing", (u, n) => this.emitUnionJSONPartial(u, n));
this.ensureBlankLine();
this.emitSerializeClass();
}
if (this.needHelpers || (this.needAttributes && (this.haveNamedUnions || this.haveEnums))) {
if (this._needHelpers || (this._needAttributes && (this.haveNamedUnions || this.haveEnums))) {
this.ensureBlankLine();
this.emitConverterClass();
}
}
protected needNamespace(): boolean {
return this._needHelpers || this._needAttributes;
}
protected makeHandlebarsContextForType(t: Type): StringMap {
const ctx = super.makeHandlebarsContextForType(t);
if (t.kind === "enum") {

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

@ -274,7 +274,14 @@ class JSONFixture extends LanguageFixture {
class JSONToXToYFixture extends JSONFixture {
private readonly runLanguage: languages.Language;
constructor(private readonly _fixturePrefix: string, languageXName: string, languageXOutputFilename: string, rendererOptions: RendererOptions, skipJSON: string[], language: languages.Language) {
constructor(
private readonly _fixturePrefix: string,
languageXName: string,
languageXOutputFilename: string,
rendererOptions: RendererOptions,
skipJSON: string[],
language: languages.Language
) {
super({
name: languageXName,
base: language.base,
@ -304,7 +311,13 @@ class JSONToXToYFixture extends JSONFixture {
async test(filename: string, additionalRendererOptions: RendererOptions, _additionalFiles: string[]) {
// Generate code for Y from X
await quicktypeForLanguage(this.runLanguage, this.language.output, this.language.name, false, additionalRendererOptions);
await quicktypeForLanguage(
this.runLanguage,
this.language.output,
this.language.name,
false,
additionalRendererOptions
);
// Parse the sample with the code generated from its schema, and compare to the sample
compareJsonFileToJson({
@ -326,7 +339,7 @@ class JSONSchemaJSONFixture extends JSONToXToYFixture {
"blns-object.json", // AJV refuses to even "compile" the schema we generate
"31189.json", // same here
"ed095.json" // same here on Travis
]
];
super("schema-json", "schema", "schema.json", {}, skipJSON, language);
}
@ -572,7 +585,7 @@ export const allFixtures: Fixture[] = [
new JSONFixture(languages.FlowLanguage),
new JSONFixture(languages.JavaScriptLanguage),
new JSONSchemaJSONFixture(languages.CSharpLanguage),
new JSONTypeScriptFixture(languages.CSharpLanguage),
new JSONTypeScriptFixture(languages.CSharpPermissibleLanguage),
new JSONSchemaFixture(languages.CSharpLanguage),
new JSONSchemaFixture(languages.JavaLanguage),
new JSONSchemaFixture(languages.GoLanguage),

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

@ -22,40 +22,51 @@ export interface Language {
sourceFiles?: string[];
}
export const CSharpLanguage: Language = {
name: "csharp",
base: "test/fixtures/csharp",
// https://github.com/dotnet/cli/issues/1582
setupCommand: "dotnet restore --no-cache",
runCommand(sample: string) {
return `dotnet run "${sample}"`;
},
diffViaSchema: true,
skipDiffViaSchema: [
"keywords.json",
"00c36.json",
"0a91a.json",
"34702.json",
"2df80.json",
"7fbfb.json",
"cda6c.json",
"c8c7e.json",
"e53b5.json"
],
allowMissingNull: false,
output: "QuickType.cs",
topLevel: "TopLevel",
skipJSON: [
"31189.json" // JSON.NET doesn't accept year 0000 as 1BC, though it should
],
skipMiscJSON: false,
skipSchema: [
"top-level-enum.schema" // The code we generate for top-level enums is incompatible with the driver
],
rendererOptions: {},
quickTestRendererOptions: [{ "array-type": "list" }, { "csharp-version": "5" }, { density: "dense" }],
sourceFiles: ["src/Language/CSharp.ts"]
};
function makeCSharpLanguage(strictOptionalVsNullable: boolean): Language {
const lang: Language = {
name: "csharp",
base: "test/fixtures/csharp",
// https://github.com/dotnet/cli/issues/1582
setupCommand: "dotnet restore --no-cache",
runCommand(sample: string) {
return `dotnet run "${sample}"`;
},
diffViaSchema: true,
skipDiffViaSchema: [
"keywords.json",
"00c36.json",
"0a91a.json",
"34702.json",
"2df80.json",
"7fbfb.json",
"cda6c.json",
"c8c7e.json",
"e53b5.json"
],
allowMissingNull: false,
output: "QuickType.cs",
topLevel: "TopLevel",
skipJSON: [
"31189.json" // JSON.NET doesn't accept year 0000 as 1BC, though it should
],
skipMiscJSON: false,
skipSchema: [
"top-level-enum.schema" // The code we generate for top-level enums is incompatible with the driver
],
rendererOptions: {},
quickTestRendererOptions: [{ "array-type": "list" }, { "csharp-version": "5" }, { density: "dense" }],
sourceFiles: ["src/Language/CSharp.ts"]
};
if (strictOptionalVsNullable) {
lang.rendererOptions = { "check-required": "true" };
} else {
lang.allowMissingNull = true;
}
return lang;
}
export const CSharpLanguage = makeCSharpLanguage(true);
export const CSharpPermissibleLanguage = makeCSharpLanguage(false);
export const JavaLanguage: Language = {
name: "java",