From 35be5b5bcf294f8c38e429feba2002c36820f5fd Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Sat, 30 Dec 2023 06:12:27 -0800 Subject: [PATCH 1/4] Add JsonSchema support to fastify plugin. https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/#serialization I opted to only handle response schemas since (from my understanding) that is where most of the performance gain is at using `fast-json-stringify`. The request schemas are mostly for validation, which Facility can already take care of for us. --- .../src/fastify/conformanceApiPlugin.ts | 253 ++++++++++++++++++ .../src/fastify/jsConformanceApiPlugin.js | 253 ++++++++++++++++++ .../JavaScriptGenerator.cs | 124 +++++++++ 3 files changed, 630 insertions(+) diff --git a/conformance/src/fastify/conformanceApiPlugin.ts b/conformance/src/fastify/conformanceApiPlugin.ts index dc1d797..21144f0 100644 --- a/conformance/src/fastify/conformanceApiPlugin.ts +++ b/conformance/src/fastify/conformanceApiPlugin.ts @@ -41,6 +41,10 @@ export type ConformanceApiPluginOptions = RegisterOptions & { export const conformanceApiPlugin: FastifyPluginAsync = async (fastify, opts) => { const { api, caseInsenstiveQueryStringKeys, includeErrorDetails } = opts; + for (const jsonSchema of jsonSchemas) { + fastify.addSchema(jsonSchema); + } + fastify.setErrorHandler((error, req, res) => { req.log.error(error); if (includeErrorDetails) { @@ -76,6 +80,17 @@ export const conformanceApiPlugin: FastifyPluginAsync { const { api, caseInsenstiveQueryStringKeys, includeErrorDetails } = opts; + for (const jsonSchema of jsonSchemas) { + fastify.addSchema(jsonSchema); + } + fastify.setErrorHandler((error, req, res) => { req.log.error(error); if (includeErrorDetails) { @@ -69,6 +73,17 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/', method: 'GET', + schema: { + response: { + 200: { + type: 'object', + properties: { + service: { type: 'string' }, + version: { type: 'string' }, + }, + }, + }, + }, handler: async function (req, res) { const request = {}; @@ -92,6 +107,16 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/widgets', method: 'GET', + schema: { + response: { + 200: { + type: 'object', + properties: { + widgets: { type: 'array', items: { $ref: 'Widget' } }, + }, + }, + }, + }, handler: async function (req, res) { const request = {}; @@ -118,6 +143,11 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/widgets', method: 'POST', + schema: { + response: { + 201: { $ref: 'Widget' }, + }, + }, handler: async function (req, res) { const request = {}; @@ -148,6 +178,12 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/widgets/:id', method: 'GET', + schema: { + response: { + 200: { $ref: 'Widget' }, + 304: { type: 'boolean' }, + }, + }, handler: async function (req, res) { const request = {}; @@ -186,6 +222,13 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/widgets/:id', method: 'DELETE', + schema: { + response: { + 204: { type: 'object', additionalProperties: false }, + 404: { type: 'boolean' }, + 409: { type: 'boolean' }, + }, + }, handler: async function (req, res) { const request = {}; @@ -225,6 +268,11 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/widgets/get', method: 'POST', + schema: { + response: { + 200: { type: 'array', items: { type: 'object', properties: { value: { $ref: 'Widget' }, error: { $ref: '_error' } } } }, + }, + }, handler: async function (req, res) { const request = {}; @@ -252,6 +300,17 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/mirrorFields', method: 'POST', + schema: { + response: { + 200: { + type: 'object', + properties: { + field: { $ref: 'Any' }, + matrix: { type: 'array', items: { type: 'array', items: { type: 'array', items: { type: 'number' } } } }, + }, + }, + }, + }, handler: async function (req, res) { const request = {}; @@ -279,6 +338,11 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/checkQuery', method: 'GET', + schema: { + response: { + 200: { type: 'object', additionalProperties: false }, + }, + }, handler: async function (req, res) { const request = {}; @@ -312,6 +376,11 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/checkPath/:string/:boolean/:double/:int32/:int64/:decimal/:enum/:datetime', method: 'GET', + schema: { + response: { + 200: { type: 'object', additionalProperties: false }, + }, + }, handler: async function (req, res) { const request = {}; @@ -345,6 +414,11 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/mirrorHeaders', method: 'GET', + schema: { + response: { + 200: { type: 'object', additionalProperties: false }, + }, + }, handler: async function (req, res) { const request = {}; @@ -387,6 +461,18 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/mixed/:path', method: 'POST', + schema: { + response: { + 200: { + type: 'object', + properties: { + normal: { type: 'string' }, + }, + }, + 202: { type: 'object', additionalProperties: true }, + 204: { type: 'boolean' }, + }, + }, handler: async function (req, res) { const request = {}; @@ -434,6 +520,16 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/required', method: 'POST', + schema: { + response: { + 200: { + type: 'object', + properties: { + normal: { type: 'string' }, + }, + }, + }, + }, handler: async function (req, res) { const request = {}; @@ -471,6 +567,11 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/mirrorBytes', method: 'POST', + schema: { + response: { + 200: { type: 'string' }, + }, + }, handler: async function (req, res) { const request = {}; @@ -503,6 +604,11 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/mirrorText', method: 'POST', + schema: { + response: { + 200: { type: 'string' }, + }, + }, handler: async function (req, res) { const request = {}; @@ -535,6 +641,11 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { fastify.route({ url: '/bodyTypes', method: 'POST', + schema: { + response: { + 200: { type: 'string' }, + }, + }, handler: async function (req, res) { const request = {}; @@ -559,3 +670,145 @@ export const jsConformanceApiPlugin = async (fastify, opts) => { } }); } + +const jsonSchemas = [ + { + $id: '_error', + type: 'object', + properties: { + code: { type: 'string' }, + message: { type: 'string' }, + details: { type: 'object', additionalProperties: true }, + innerError: { $ref: '_error' }, + } + }, + { + $id: 'Widget', + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + } + }, + { + $id: 'Any', + type: 'object', + properties: { + string: { type: 'string' }, + boolean: { type: 'boolean' }, + double: { type: 'number' }, + int32: { type: 'integer' }, + int64: { type: 'integer' }, + decimal: { type: 'number' }, + datetime: { type: 'string' }, + bytes: { type: 'string' }, + object: { type: 'object', additionalProperties: true }, + error: { $ref: '_error' }, + data: { $ref: 'Any' }, + enum: { $ref: 'Answer' }, + array: { $ref: 'AnyArray' }, + map: { $ref: 'AnyMap' }, + result: { $ref: 'AnyResult' }, + nullable: { $ref: 'AnyNullable' }, + } + }, + { + $id: 'AnyArray', + type: 'object', + properties: { + string: { type: 'array', items: { type: 'string' } }, + boolean: { type: 'array', items: { type: 'boolean' } }, + double: { type: 'array', items: { type: 'number' } }, + int32: { type: 'array', items: { type: 'integer' } }, + int64: { type: 'array', items: { type: 'integer' } }, + decimal: { type: 'array', items: { type: 'number' } }, + datetime: { type: 'array', items: { type: 'string' } }, + bytes: { type: 'array', items: { type: 'string' } }, + object: { type: 'array', items: { type: 'object', additionalProperties: true } }, + error: { type: 'array', items: { $ref: '_error' } }, + data: { type: 'array', items: { $ref: 'Any' } }, + enum: { type: 'array', items: { $ref: 'Answer' } }, + array: { type: 'array', items: { type: 'array', items: { type: 'integer' } } }, + map: { type: 'array', items: { type: 'object', additionalProperties: { type: 'integer' } } }, + result: { type: 'array', items: { type: 'object', properties: { value: { type: 'integer' }, error: { $ref: '_error' } } } }, + nullable: { type: 'array', items: { oneOf: [ { type: 'integer' }, { type: 'null' } ] } }, + } + }, + { + $id: 'AnyMap', + type: 'object', + properties: { + string: { type: 'object', additionalProperties: { type: 'string' } }, + boolean: { type: 'object', additionalProperties: { type: 'boolean' } }, + double: { type: 'object', additionalProperties: { type: 'number' } }, + int32: { type: 'object', additionalProperties: { type: 'integer' } }, + int64: { type: 'object', additionalProperties: { type: 'integer' } }, + decimal: { type: 'object', additionalProperties: { type: 'number' } }, + datetime: { type: 'object', additionalProperties: { type: 'string' } }, + bytes: { type: 'object', additionalProperties: { type: 'string' } }, + object: { type: 'object', additionalProperties: { type: 'object', additionalProperties: true } }, + error: { type: 'object', additionalProperties: { $ref: '_error' } }, + data: { type: 'object', additionalProperties: { $ref: 'Any' } }, + enum: { type: 'object', additionalProperties: { $ref: 'Answer' } }, + array: { type: 'object', additionalProperties: { type: 'array', items: { type: 'integer' } } }, + map: { type: 'object', additionalProperties: { type: 'object', additionalProperties: { type: 'integer' } } }, + result: { type: 'object', additionalProperties: { type: 'object', properties: { value: { type: 'integer' }, error: { $ref: '_error' } } } }, + nullable: { type: 'object', additionalProperties: { oneOf: [ { type: 'integer' }, { type: 'null' } ] } }, + } + }, + { + $id: 'AnyResult', + type: 'object', + properties: { + string: { type: 'object', properties: { value: { type: 'string' }, error: { $ref: '_error' } } }, + boolean: { type: 'object', properties: { value: { type: 'boolean' }, error: { $ref: '_error' } } }, + double: { type: 'object', properties: { value: { type: 'number' }, error: { $ref: '_error' } } }, + int32: { type: 'object', properties: { value: { type: 'integer' }, error: { $ref: '_error' } } }, + int64: { type: 'object', properties: { value: { type: 'integer' }, error: { $ref: '_error' } } }, + decimal: { type: 'object', properties: { value: { type: 'number' }, error: { $ref: '_error' } } }, + datetime: { type: 'object', properties: { value: { type: 'string' }, error: { $ref: '_error' } } }, + bytes: { type: 'object', properties: { value: { type: 'string' }, error: { $ref: '_error' } } }, + object: { type: 'object', properties: { value: { type: 'object', additionalProperties: true }, error: { $ref: '_error' } } }, + error: { type: 'object', properties: { value: { $ref: '_error' }, error: { $ref: '_error' } } }, + data: { type: 'object', properties: { value: { $ref: 'Any' }, error: { $ref: '_error' } } }, + enum: { type: 'object', properties: { value: { $ref: 'Answer' }, error: { $ref: '_error' } } }, + array: { type: 'object', properties: { value: { type: 'array', items: { type: 'integer' } }, error: { $ref: '_error' } } }, + map: { type: 'object', properties: { value: { type: 'object', additionalProperties: { type: 'integer' } }, error: { $ref: '_error' } } }, + result: { type: 'object', properties: { value: { type: 'object', properties: { value: { type: 'integer' }, error: { $ref: '_error' } } }, error: { $ref: '_error' } } }, + nullable: { type: 'object', properties: { value: { oneOf: [ { type: 'integer' }, { type: 'null' } ] }, error: { $ref: '_error' } } }, + } + }, + { + $id: 'AnyNullable', + type: 'object', + properties: { + string: { oneOf: [ { type: 'string' }, { type: 'null' } ] }, + boolean: { oneOf: [ { type: 'boolean' }, { type: 'null' } ] }, + double: { oneOf: [ { type: 'number' }, { type: 'null' } ] }, + int32: { oneOf: [ { type: 'integer' }, { type: 'null' } ] }, + int64: { oneOf: [ { type: 'integer' }, { type: 'null' } ] }, + decimal: { oneOf: [ { type: 'number' }, { type: 'null' } ] }, + datetime: { oneOf: [ { type: 'string' }, { type: 'null' } ] }, + bytes: { oneOf: [ { type: 'string' }, { type: 'null' } ] }, + object: { oneOf: [ { type: 'object', additionalProperties: true }, { type: 'null' } ] }, + error: { oneOf: [ { $ref: '_error' }, { type: 'null' } ] }, + data: { oneOf: [ { $ref: 'Any' }, { type: 'null' } ] }, + enum: { oneOf: [ { $ref: 'Answer' }, { type: 'null' } ] }, + array: { oneOf: [ { type: 'array', items: { type: 'integer' } }, { type: 'null' } ] }, + map: { oneOf: [ { type: 'object', additionalProperties: { type: 'integer' } }, { type: 'null' } ] }, + result: { oneOf: [ { type: 'object', properties: { value: { type: 'integer' }, error: { $ref: '_error' } } }, { type: 'null' } ] }, + } + }, + { + $id: 'HasWidget', + type: 'object', + properties: { + widget: { $ref: 'Widget' }, + } + }, + { + $id: 'Answer', + type: 'string', + enum: [ 'yes', 'no', 'maybe' ], + }, +]; diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 6d8b11d..8559dc4 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -543,6 +543,7 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) var capModuleName = CodeGenUtility.Capitalize(moduleName); var camelCaseModuleName = CodeGenUtility.ToCamelCase(moduleName); var pluginFileName = CodeGenUtility.Uncapitalize(moduleName) + "Plugin" + (TypeScript ? ".ts" : ".js"); + var customTypes = new HashSet(service.Dtos.Select(x => x.Name).Concat(service.Enums.Select(x => x.Name))); var file = CreateFile(pluginFileName, code => { @@ -592,6 +593,10 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) { code.WriteLine("const { api, caseInsenstiveQueryStringKeys, includeErrorDetails } = opts;"); + code.WriteLine(); + using (code.Block("for (const jsonSchema of jsonSchemas) {", "}")) + code.WriteLine("fastify.addSchema(jsonSchema);"); + code.WriteLine(); using (code.Block("fastify.setErrorHandler((error, req, res) => {", "});")) { @@ -653,6 +658,38 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) { code.WriteLine($"url: '{fastifyPath}',"); code.WriteLine($"method: '{httpMethodInfo.Method.ToUpperInvariant()}',"); + using (code.Block("schema: {", "},")) + { + using (code.Block("response: {", "},")) + { + foreach (var response in httpMethodInfo.ValidResponses) + { + var statusCode = (int) response.StatusCode; + code.Write($"{statusCode}: "); + + if (response.BodyField is not null) + { + code.WriteLine($"{GetJsonSchemaType(response.BodyField.ServiceField.TypeName)},"); + } + else if (response.NormalFields?.Count > 0) + { + using (code.Block("{", "},")) + { + code.WriteLine("type: 'object',"); + using (code.Block("properties: {", "},")) + { + foreach (var normalField in response.NormalFields) + code.WriteLine($"{normalField.ServiceField.Name}: {GetJsonSchemaType(normalField.ServiceField.TypeName)},"); + } + } + } + else + { + code.WriteLine("{ type: 'object', additionalProperties: false },"); + } + } + } + } using (code.Block("handler: async function (req, res) {", "}")) { code.WriteLine("const request" + IfTypeScript($": I{capMethodName}Request") + " = {};"); @@ -775,11 +812,98 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) } } + WriteJsonSchemaDtos(code, service); + if (TypeScript) WriteTypes(code, httpServiceInfo); }); return new CodeGenOutput(file); + + string GetJsonSchemaType(string typeName) + { + if (typeName.EndsWith("[]", StringComparison.Ordinal)) + return $"{{ type: 'array', items: {GetJsonSchemaType(typeName.Substring(0, typeName.Length - 2))} }}"; + if (typeName.StartsWith("nullable<", StringComparison.Ordinal)) + return $"{{ oneOf: [ {GetJsonSchemaType(typeName.Substring(9, typeName.Length - 10))}, {{ type: 'null' }} ] }}"; + if (typeName.StartsWith("result<", StringComparison.Ordinal)) + return $"{{ type: 'object', properties: {{ value: {GetJsonSchemaType(typeName.Substring(7, typeName.Length - 8))}, error: {{ $ref: '_error' }} }} }}"; + if (typeName.StartsWith("map<", StringComparison.Ordinal)) + return $"{{ type: 'object', additionalProperties: {GetJsonSchemaType(typeName.Substring(4, typeName.Length - 5))} }}"; + if (typeName is "error") + return $"{{ $ref: '{"_error"}' }}"; + if (customTypes.Contains(typeName)) + return $"{{ $ref: '{typeName}' }}"; + if (typeName is "string" or "bytes" or "datetime") + return $"{{ type: '{"string"}' }}"; + if (typeName is "boolean") + return $"{{ type: '{"boolean"}' }}"; + if (typeName is "int32" or "int64") + return $"{{ type: '{"integer"}' }}"; + if (typeName is "decimal" or "double") + return $"{{ type: '{"number"}' }}"; + if (typeName is "object") + return $"{{ type: '{"object"}', additionalProperties: true }}"; + + throw new NotSupportedException("Unknown type " + typeName); + } + + void WriteJsonSchemaDtos(CodeWriter code, ServiceInfo service) + { + code.WriteLine(); + using (code.Block("const jsonSchemas = [", $"]{IfTypeScript(" as const")};")) + { + using (code.Block("{", $"}}{IfTypeScript(" as const")},")) + { + code.WriteLine("$id: '_error',"); + code.WriteLine("type: 'object',"); + using (code.Block("properties: {", "}")) + { + code.WriteLine("code: { type: 'string' },"); + code.WriteLine("message: { type: 'string' },"); + code.WriteLine("details: { type: 'object', additionalProperties: true },"); + code.WriteLine("innerError: { $ref: '_error' },"); + } + } + + foreach (var dto in service.Dtos) + { + using (code.Block("{", $"}}{IfTypeScript(" as const")},")) + { + code.WriteLine($"$id: '{dto.Name}',"); + code.WriteLine("type: 'object',"); + using (code.Block("properties: {", "}")) + { + foreach (var field in dto.Fields) + code.WriteLine($"{field.Name}: {GetJsonSchemaType(field.TypeName)},"); + } + } + } + + foreach (var enumInfo in service.Enums) + { + using (code.Block("{", $"}}{IfTypeScript(" as const")},")) + { + code.WriteLine($"$id: '{enumInfo.Name}',"); + code.WriteLine("type: 'string',"); + code.Write("enum: [ "); + + var shouldWriteComma = false; + foreach (var enumValue in enumInfo.Values) + { + if (shouldWriteComma) + code.Write(", "); + else + shouldWriteComma = true; + + code.Write($"'{enumValue.Name}'"); + } + + code.WriteLine(" ],"); + } + } + } + } } private void WriteFileHeader(CodeWriter code) From a00bbc0580d11f5fcc19a9dfad7c1b3f94258b8e Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Mon, 1 Jan 2024 06:14:13 -0800 Subject: [PATCH 2/4] Switch on `ServiceTypeKind` instead of digging into the type name directly. --- .../JavaScriptGenerator.cs | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 8559dc4..df03de8 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -669,7 +669,7 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) if (response.BodyField is not null) { - code.WriteLine($"{GetJsonSchemaType(response.BodyField.ServiceField.TypeName)},"); + code.WriteLine($"{GetJsonSchemaType(service.GetFieldType(response.BodyField.ServiceField))},"); } else if (response.NormalFields?.Count > 0) { @@ -679,7 +679,7 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) using (code.Block("properties: {", "},")) { foreach (var normalField in response.NormalFields) - code.WriteLine($"{normalField.ServiceField.Name}: {GetJsonSchemaType(normalField.ServiceField.TypeName)},"); + code.WriteLine($"{normalField.ServiceField.Name}: {GetJsonSchemaType(service.GetFieldType(normalField.ServiceField))},"); } } } @@ -820,32 +820,27 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) return new CodeGenOutput(file); - string GetJsonSchemaType(string typeName) + string GetJsonSchemaType(ServiceTypeInfo? serviceType) { - if (typeName.EndsWith("[]", StringComparison.Ordinal)) - return $"{{ type: 'array', items: {GetJsonSchemaType(typeName.Substring(0, typeName.Length - 2))} }}"; - if (typeName.StartsWith("nullable<", StringComparison.Ordinal)) - return $"{{ oneOf: [ {GetJsonSchemaType(typeName.Substring(9, typeName.Length - 10))}, {{ type: 'null' }} ] }}"; - if (typeName.StartsWith("result<", StringComparison.Ordinal)) - return $"{{ type: 'object', properties: {{ value: {GetJsonSchemaType(typeName.Substring(7, typeName.Length - 8))}, error: {{ $ref: '_error' }} }} }}"; - if (typeName.StartsWith("map<", StringComparison.Ordinal)) - return $"{{ type: 'object', additionalProperties: {GetJsonSchemaType(typeName.Substring(4, typeName.Length - 5))} }}"; - if (typeName is "error") - return $"{{ $ref: '{"_error"}' }}"; - if (customTypes.Contains(typeName)) - return $"{{ $ref: '{typeName}' }}"; - if (typeName is "string" or "bytes" or "datetime") - return $"{{ type: '{"string"}' }}"; - if (typeName is "boolean") - return $"{{ type: '{"boolean"}' }}"; - if (typeName is "int32" or "int64") - return $"{{ type: '{"integer"}' }}"; - if (typeName is "decimal" or "double") - return $"{{ type: '{"number"}' }}"; - if (typeName is "object") - return $"{{ type: '{"object"}', additionalProperties: true }}"; - - throw new NotSupportedException("Unknown type " + typeName); + if (serviceType is null) + throw new ArgumentNullException(nameof(serviceType)); + + return serviceType.Kind switch + { + ServiceTypeKind.String or ServiceTypeKind.Bytes or ServiceTypeKind.DateTime or ServiceTypeKind.ExternalEnum => $"{{ type: '{"string"}' }}", + ServiceTypeKind.Boolean => $"{{ type: '{"boolean"}' }}", + ServiceTypeKind.Double or ServiceTypeKind.Decimal => $"{{ type: '{"number"}' }}", + ServiceTypeKind.Int32 or ServiceTypeKind.Int64 => $"{{ type: '{"integer"}' }}", + ServiceTypeKind.Object or ServiceTypeKind.ExternalDto => $"{{ type: '{"object"}', additionalProperties: true }}", + ServiceTypeKind.Error => $"{{ $ref: '{"_error"}' }}", + ServiceTypeKind.Dto => $"{{ $ref: '{serviceType.Dto!.Name}' }}", + ServiceTypeKind.Enum => $"{{ $ref: '{serviceType.Enum!.Name}' }}", + ServiceTypeKind.Result => $"{{ type: 'object', properties: {{ value: {GetJsonSchemaType(serviceType.ValueType!)}, error: {{ $ref: '_error' }} }} }}", + ServiceTypeKind.Array => $"{{ type: 'array', items: {GetJsonSchemaType(serviceType.ValueType!)} }}", + ServiceTypeKind.Map => $"{{ type: 'object', additionalProperties: {GetJsonSchemaType(serviceType.ValueType!)} }}", + ServiceTypeKind.Nullable => $"{{ oneOf: [ {GetJsonSchemaType(serviceType.ValueType!)}, {{ type: 'null' }} ] }}", + _ => throw new NotSupportedException($"Unsupported service type '{serviceType.Kind}'"), + }; } void WriteJsonSchemaDtos(CodeWriter code, ServiceInfo service) @@ -875,7 +870,7 @@ void WriteJsonSchemaDtos(CodeWriter code, ServiceInfo service) using (code.Block("properties: {", "}")) { foreach (var field in dto.Fields) - code.WriteLine($"{field.Name}: {GetJsonSchemaType(field.TypeName)},"); + code.WriteLine($"{field.Name}: {GetJsonSchemaType(service.GetFieldType(field))},"); } } } From 51f5c1f2d35349c5037b5613cb3740cd934dc7a4 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Mon, 1 Jan 2024 14:20:25 -0800 Subject: [PATCH 3/4] Assume non-null value from `GetFieldType`. --- .../JavaScriptGenerator.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index df03de8..cc24567 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -669,7 +669,7 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) if (response.BodyField is not null) { - code.WriteLine($"{GetJsonSchemaType(service.GetFieldType(response.BodyField.ServiceField))},"); + code.WriteLine($"{GetJsonSchemaType(service.GetFieldType(response.BodyField.ServiceField)!)},"); } else if (response.NormalFields?.Count > 0) { @@ -679,7 +679,7 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) using (code.Block("properties: {", "},")) { foreach (var normalField in response.NormalFields) - code.WriteLine($"{normalField.ServiceField.Name}: {GetJsonSchemaType(service.GetFieldType(normalField.ServiceField))},"); + code.WriteLine($"{normalField.ServiceField.Name}: {GetJsonSchemaType(service.GetFieldType(normalField.ServiceField)!)},"); } } } @@ -820,11 +820,8 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) return new CodeGenOutput(file); - string GetJsonSchemaType(ServiceTypeInfo? serviceType) + string GetJsonSchemaType(ServiceTypeInfo serviceType) { - if (serviceType is null) - throw new ArgumentNullException(nameof(serviceType)); - return serviceType.Kind switch { ServiceTypeKind.String or ServiceTypeKind.Bytes or ServiceTypeKind.DateTime or ServiceTypeKind.ExternalEnum => $"{{ type: '{"string"}' }}", @@ -870,7 +867,7 @@ void WriteJsonSchemaDtos(CodeWriter code, ServiceInfo service) using (code.Block("properties: {", "}")) { foreach (var field in dto.Fields) - code.WriteLine($"{field.Name}: {GetJsonSchemaType(service.GetFieldType(field))},"); + code.WriteLine($"{field.Name}: {GetJsonSchemaType(service.GetFieldType(field)!)},"); } } } From 580c68bdc525f194adfa7923f1759673899d52a1 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Mon, 1 Jan 2024 14:27:17 -0800 Subject: [PATCH 4/4] Don't use interpolated strings to prevent analysis warning. Instead we just set it to a suggestion in `.editorconfig` --- .editorconfig | 1 + .../JavaScriptGenerator.cs | 33 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.editorconfig b/.editorconfig index 6f31936..71b4663 100644 --- a/.editorconfig +++ b/.editorconfig @@ -208,6 +208,7 @@ dotnet_diagnostic.IDE0161.severity = suggestion dotnet_diagnostic.IDE0170.severity = suggestion dotnet_diagnostic.IDE0180.severity = warning dotnet_diagnostic.IDE1005.severity = suggestion +dotnet_diagnostic.JSON002.severity = suggestion dotnet_diagnostic.SA0001.severity = none dotnet_diagnostic.SA1003.severity = none dotnet_diagnostic.SA1008.severity = none diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index cc24567..5c7e023 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -820,25 +820,22 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) return new CodeGenOutput(file); - string GetJsonSchemaType(ServiceTypeInfo serviceType) + string GetJsonSchemaType(ServiceTypeInfo serviceType) => serviceType.Kind switch { - return serviceType.Kind switch - { - ServiceTypeKind.String or ServiceTypeKind.Bytes or ServiceTypeKind.DateTime or ServiceTypeKind.ExternalEnum => $"{{ type: '{"string"}' }}", - ServiceTypeKind.Boolean => $"{{ type: '{"boolean"}' }}", - ServiceTypeKind.Double or ServiceTypeKind.Decimal => $"{{ type: '{"number"}' }}", - ServiceTypeKind.Int32 or ServiceTypeKind.Int64 => $"{{ type: '{"integer"}' }}", - ServiceTypeKind.Object or ServiceTypeKind.ExternalDto => $"{{ type: '{"object"}', additionalProperties: true }}", - ServiceTypeKind.Error => $"{{ $ref: '{"_error"}' }}", - ServiceTypeKind.Dto => $"{{ $ref: '{serviceType.Dto!.Name}' }}", - ServiceTypeKind.Enum => $"{{ $ref: '{serviceType.Enum!.Name}' }}", - ServiceTypeKind.Result => $"{{ type: 'object', properties: {{ value: {GetJsonSchemaType(serviceType.ValueType!)}, error: {{ $ref: '_error' }} }} }}", - ServiceTypeKind.Array => $"{{ type: 'array', items: {GetJsonSchemaType(serviceType.ValueType!)} }}", - ServiceTypeKind.Map => $"{{ type: 'object', additionalProperties: {GetJsonSchemaType(serviceType.ValueType!)} }}", - ServiceTypeKind.Nullable => $"{{ oneOf: [ {GetJsonSchemaType(serviceType.ValueType!)}, {{ type: 'null' }} ] }}", - _ => throw new NotSupportedException($"Unsupported service type '{serviceType.Kind}'"), - }; - } + ServiceTypeKind.String or ServiceTypeKind.Bytes or ServiceTypeKind.DateTime or ServiceTypeKind.ExternalEnum => "{ type: 'string' }", + ServiceTypeKind.Boolean => "{ type: 'boolean' }", + ServiceTypeKind.Double or ServiceTypeKind.Decimal => "{ type: 'number' }", + ServiceTypeKind.Int32 or ServiceTypeKind.Int64 => "{ type: 'integer' }", + ServiceTypeKind.Object or ServiceTypeKind.ExternalDto => "{ type: 'object', additionalProperties: true }", + ServiceTypeKind.Error => "{ $ref: '_error' }", + ServiceTypeKind.Dto => $"{{ $ref: '{serviceType.Dto!.Name}' }}", + ServiceTypeKind.Enum => $"{{ $ref: '{serviceType.Enum!.Name}' }}", + ServiceTypeKind.Result => $"{{ type: 'object', properties: {{ value: {GetJsonSchemaType(serviceType.ValueType!)}, error: {{ $ref: '_error' }} }} }}", + ServiceTypeKind.Array => $"{{ type: 'array', items: {GetJsonSchemaType(serviceType.ValueType!)} }}", + ServiceTypeKind.Map => $"{{ type: 'object', additionalProperties: {GetJsonSchemaType(serviceType.ValueType!)} }}", + ServiceTypeKind.Nullable => $"{{ oneOf: [ {GetJsonSchemaType(serviceType.ValueType!)}, {{ type: 'null' }} ] }}", + _ => throw new NotSupportedException($"Unknown field type '{serviceType.Kind}'"), + }; void WriteJsonSchemaDtos(CodeWriter code, ServiceInfo service) {