UNPKG

@udraft/core

Version:

uDraft is a language and stack agnostic code-generation tool that simplifies full-stack development by converting a single YAML file into code for rapid development.

322 lines (277 loc) 9.35 kB
import * as Case from "case"; import * as path from "path"; import { UFeature } from "../entities/feature"; import { UModel } from "../entities/model"; import { UModule } from "../entities/module"; import { URenderer } from "../entities/renderer"; import { addPubspecDependency } from "../helpers/package"; import { $attr } from "../shortcuts/queries"; import { RenderContent, RenderPath, RenderSelection } from "../types/renderer"; import DartClassRenderer from "./dart-class-renderer"; import { _http, _rootModule } from "../shortcuts/attributes"; const KEYS = { pubspec: "pubspec", baseApiClient: "ApiClient", }; const DEFAULT_API_CLIENT_CODE = ` import 'package:dio/dio.dart'; class ApiClient { final Dio dio; ApiClient({required this.dio}); Future<dynamic> request( String path, { required String method, String? contentType, Map<String, dynamic>? queryParameters, dynamic data, }) async { try { final response = await dio.request( path, options: Options( method: method, contentType: contentType, ), queryParameters: queryParameters, data: data, ); return response.data; } on DioException catch (e) { throw ApiException( code: e.response?.statusCode, message: e.message ?? 'Unknown error', ); } } } class ApiResponse<T> { final T? data; final String? error; final int? statusCode; ApiResponse.success(this.data) : statusCode = 200, error = null; ApiResponse.error({this.statusCode, String? message}) : data = null, error = message ?? 'Unknown error'; bool get isSuccess => error == null; } class ApiException implements Exception { final int? code; final String message; ApiException({this.code, required this.message}); @override String toString() => message; } `.trim(); export default class DartApiClientRenderer extends URenderer { private _serviceDir = "lib/apis"; private _baseClientPath = "lib/core/api_client.dart"; private _where?: (module: UModule, feature: UFeature) => boolean; constructor(options?: { serviceDir?: string; baseClientPath?: string; where?: (module: UModule, feature: UFeature) => boolean; }) { super("dart@api-client"); if (options?.serviceDir) this._serviceDir = options.serviceDir; if (options?.baseClientPath) this._baseClientPath = options.baseClientPath; this._where = options?.where; } $moduleServiceName(module: UModule) { return `${Case.pascal(module.$name())}Api`; } $fileName(module: UModule, extension = true) { return `${Case.snake(module.$name())}_api${extension ? ".dart" : ""}`; } private resolveFeatureRoute(module: UModule, feature: UFeature): string { const moduleConfig = $attr(module, _http()); const featureConfig = $attr(feature, _http()); return ( (moduleConfig?.url || "") + "/" + (featureConfig?.url || "") ).replace(/\/{2,}/, "/"); } private getHttpMethod(feature: UFeature): string { return ($attr(feature, _http())?.method || "get").toUpperCase(); } async select(): Promise<RenderSelection> { const modules: UModule[] = []; const features: UFeature[] = []; this.$features( (m, f) => !!$attr(f, _http()) && (this._where ? this._where(m, f) : true) ).forEach((feature) => { const mod = $attr(feature, _rootModule()); if (mod && !modules.find((m) => m.$name() == mod.$name())) modules.push(mod); if (!features.find((f) => f.$name() == feature.$name())) features.push(feature); }); const paths: RenderPath[] = [ { key: KEYS.pubspec, path: "pubspec.yaml" }, { key: KEYS.baseApiClient, path: this._baseClientPath, meta: { isBase: true }, }, ]; modules.forEach((module) => { if (module.$features().length > 0) { paths.push({ key: this.$moduleServiceName(module), meta: { module: module.$name() }, path: path.join(this._serviceDir, this.$fileName(module)), }); } }); return { paths, modules, features, }; } async render(): Promise<RenderContent[]> { const output: RenderContent[] = [ { key: KEYS.pubspec, content: addPubspecDependency( this.$content(KEYS.pubspec)?.content || "", [{ name: "dio", version: "^5.4.0" }] ), }, ]; // Generate base API client if missing const baseApiClientPath = this.$path(KEYS.baseApiClient); const baseApiClient = this.$content(KEYS.baseApiClient); if (!baseApiClient?.content) { output.push({ key: KEYS.baseApiClient, content: DEFAULT_API_CLIENT_CODE, meta: { isBase: true }, }); } // Generate module APIs const modules = this.$selection().modules || []; for (const module of modules) { const features = module.$features(this.$selection().features); if (features.length === 0) continue; const serviceName = this.$moduleServiceName(module); const modulePath = this.$path(serviceName); const dartClassRenderer = this.$draft().$requireRenderer<DartClassRenderer>(this, "dart@classes"); if (!modulePath) continue; let imports = `import "${this.$resolveRelativePath( modulePath.path, baseApiClientPath?.path + "" )}";`; let importedModels: string[] = []; let methods: string[] = []; const importModel = (model: UModel) => { const modelKey = dartClassRenderer.$key(model); const modelPath = dartClassRenderer.$path(modelKey)?.path; if (importedModels.includes(modelKey)) return; imports += `\nimport "${this.$resolveRelativePath( modulePath.path, modelPath + "" )}";`; }; features.forEach((feature) => { const methodName = Case.camel(feature.$name()); const inputModel = feature.$input(); const outputModel = feature.$output(); const httpConfig = $attr(feature, _http()); const contentType = httpConfig?.contentType; const params = httpConfig?.params || {}; const method = this.getHttpMethod(feature); const route = this.resolveFeatureRoute(module, feature); // Process route with path parameters let processedRoute = route.replace(/{([^}]+)}/g, (match, param) => { const fieldName = params[param]; return inputModel && fieldName ? `\${${Case.camel(inputModel.$name())}.${fieldName}}` : match; }); // Build query parameters const queryParamEntries = (inputModel?.$fields() || []) .filter( (field) => !Object.values(params).some((n) => n == field.$name()) ) .map( (field) => `'${dartClassRenderer.$fieldName(field)}': ${Case.camel( inputModel?.$name() + "" )}.${dartClassRenderer.$fieldName(field)}` ); const queryParamsCode = queryParamEntries.length > 0 ? `queryParameters: {${queryParamEntries.join(", ")}}` : ""; // Build data based on content type // And handle input model let inputType = "void"; let dataCode = ""; if (inputModel) { const inputClassName = dartClassRenderer.$className(inputModel); importModel(inputModel); inputType = inputClassName; const inputVar = Case.camel(inputModel.$name()); dataCode = `data: ${inputVar}.toJson(),`; } // Handle output model let outputType = "void"; if (outputModel) { const outputClassName = dartClassRenderer.$className(outputModel); importModel(outputModel); outputType = outputClassName; } // Build method parameters const methodParams = inputModel ? `${inputType} ${Case.camel(inputModel.$name())}` : ""; const sendQuery = httpConfig?.noBody !== undefined ? httpConfig.noBody : ["GET", "SEARCH", "DELETE"].includes(method); // Build method implementation methods.push(` Future<ApiResponse<${outputType}>> ${methodName}(${methodParams}) async { try { ${outputModel ? "final response = " : ""}await client.request( '${processedRoute}', method: '${method}',${ contentType ? `\n contentType: '${contentType}',` : "" }${ sendQuery && queryParamsCode ? `\n ${queryParamsCode},` : "" }${!sendQuery ? `\n ${dataCode}` : ""} ); return ApiResponse.success(${ outputModel ? `${dartClassRenderer.$className(outputModel)}.fromJson(response)` : "null" }); } on ApiException catch (e) { return ApiResponse.error( statusCode: e.code, message: e.message, ); } }`); }); const content = ` ${imports} class ${serviceName} { final ApiClient client; ${serviceName}({ required this.client, }); ${methods.join("\n")} } `.trim(); output.push({ key: serviceName, content, meta: modulePath.meta, }); } return output; } }