极简接口定义和响应定义,支持更多的解析器

This commit is contained in:
Cody
2026-03-09 11:04:52 +08:00
parent a063ce178e
commit 03b89706a5
14 changed files with 482 additions and 261 deletions

View File

@@ -29,3 +29,4 @@ export 'src/domain/entities/socket_error.dart';
// Annotations代码生成
export 'src/annotations/api_request.dart';
export 'src/annotations/api_response.dart';

View File

@@ -1,49 +1,56 @@
import 'package:networks_sdk/src/domain/entities/api_request_type.dart';
import 'package:networks_sdk/src/domain/entities/http_method.dart';
/// API 请求注解 — 标记一个类为 API 请求
///
/// 配合 `build_runner` 代码生成器,自动生成 `ApiRequestable<T>` 协议实现,
/// 使用侧只需定义字段 + 注解path / method / requestType / includeToken
/// 全部由生成器自动提供。
///
/// ## 使用方式
/// ## 有响应数据(指定 responseType
///
/// ```dart
/// @ApiRequest(
/// path: '/auth/login',
/// path: ApiPaths.authLogin,
/// method: HttpMethod.post,
/// responseType: LoginData,
/// responseType: LoginResponse,
/// requestType: ApiRequestType.login,
/// )
/// @JsonSerializable()
/// class LoginRequest extends ApiRequestable<LoginData>
/// class LoginRequest extends ApiRequestable<LoginResponse>
/// with _$LoginRequestApi {
/// final String email;
/// final String password;
///
/// LoginRequest({required this.email, required this.password});
///
/// @override
/// Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
/// }
/// ```
///
/// 生成器自动生成 `_$LoginRequestApi` mixin提供
/// - `path` → `'/auth/login'`
/// - `method` → `HttpMethod.post`
/// - `requestType` → `ApiRequestType.login`
/// - `includeToken` → `false`login 类型自动设为 false
/// ## 无响应数据(省略 responseType
///
/// ```dart
/// @ApiRequest(
/// path: ApiPaths.authLogout,
/// method: HttpMethod.post,
/// )
/// class LogoutRequest extends ApiRequestable<void>
/// with _$LogoutRequestApi {
/// LogoutRequest();
/// }
/// ```
///
/// 生成器自动生成 `_$XxxRequestApi` mixin提供
/// - `path` / `method` / `requestType` / `includeToken`
/// - `toJson()` — 从声明字段自动生成
/// - `responseType` 存在时:`parameters` getter 自动注册 `fromJson`
class ApiRequest {
/// API 路径(如 `'/auth/login'`
/// API 路径(如 `ApiPaths.authLogin`
final String path;
/// HTTP 方法(默认 POST
final HttpMethod method;
/// 响应类型(用于泛型绑定
final Type responseType;
/// 响应数据类型(省略表示无响应数据,对应 `ApiRequestable<void>`
final Type? responseType;
/// 请求类型(决定 header 处理方式,默认 request
final ApiRequestType requestType;
@@ -57,7 +64,7 @@ class ApiRequest {
const ApiRequest({
required this.path,
this.method = HttpMethod.post,
required this.responseType,
this.responseType,
this.requestType = ApiRequestType.request,
this.includeToken,
this.customHeaders,

View File

@@ -0,0 +1,31 @@
/// Response DTO 无需任何注解。
///
/// 生成器从 `@ApiRequest(responseType: T)` 声明出发,自动推导并生成
/// `_$TFromJson` 反序列化函数,以及所有嵌套自定义类型的 `fromJson`。
///
/// ## 使用方式(只需标注 Request 类)
///
/// ```dart
/// // Request唯一需要注解的地方
/// @ApiRequest(path: ApiPaths.authLogin, responseType: LoginResponse)
/// class LoginRequest extends ApiRequestable<LoginResponse>
/// with _$LoginRequestApi {
/// final String email;
/// final String password;
/// LoginRequest({required this.email, required this.password});
/// }
///
/// // Response纯 Dart 类,零注解,零样板
/// class LoginResponse {
/// @JsonKey(name: 'access_token') final String accessToken;
/// final LoginProfile profile;
/// const LoginResponse({required this.accessToken, required this.profile});
/// // 生成器自动提供 _$LoginResponseFromJson
/// }
/// ```
///
/// 此文件保留作占位符,供未来扩展使用(如自定义序列化策略)。
/// 当前版本 Response 类无需标注任何注解,所有 fromJson 由生成器自动推导。
class ApiResponse {
const ApiResponse();
}

View File

@@ -20,25 +20,36 @@ const _jsonKeyChecker = TypeChecker.fromUrl(
/// 支持 `@JsonKey(name: '...')` 字段重命名。
/// 如有 `@JsonKey(includeToJson: false)` 则跳过该字段。
///
/// ## 使用模式
///
/// Request 类只需 `@ApiRequest` 注解,无需 `@JsonSerializable`
/// ## 使用模式(有响应数据)
///
/// ```dart
/// @ApiRequest(
/// path: ApiPaths.authLogin,
/// method: HttpMethod.post,
/// responseType: LoginData,
/// responseType: LoginResponse,
/// requestType: ApiRequestType.login,
/// )
/// class LoginRequest extends ApiRequestable<LoginData>
/// class LoginRequest extends ApiRequestable<LoginResponse>
/// with _$LoginRequestApi {
/// final String email;
/// final String password;
///
/// LoginRequest({required this.email, required this.password});
/// // 完毕toJson / path / method 全部由 mixin 自动生成
/// // Response 的 fromJson 在 parameters getter 中自动注册
/// // 完毕toJson / path / method / fromJson 注册全部由 mixin 自动生成
/// }
/// ```
///
/// ## 使用模式(无响应数据)
///
/// ```dart
/// @ApiRequest(
/// path: ApiPaths.authLogout,
/// method: HttpMethod.post,
/// )
/// class LogoutRequest extends ApiRequestable<void>
/// with _$LogoutRequestApi {
/// LogoutRequest();
/// // responseType 省略 → mixin 跳过 fromJson 注册
/// }
/// ```
///
@@ -46,10 +57,10 @@ const _jsonKeyChecker = TypeChecker.fromUrl(
///
/// `_$<ClassName>Api`
///
/// ## 生成示例
/// ## 生成示例(有响应数据)
///
/// ```dart
/// mixin _$LoginRequestApi on ApiRequestable<LoginData> {
/// mixin _$LoginRequestApi on ApiRequestable<LoginResponse> {
/// @override String get path => '/auth/login';
/// @override HttpMethod get method => HttpMethod.post;
/// @override ApiRequestType get requestType => ApiRequestType.login;
@@ -61,7 +72,7 @@ const _jsonKeyChecker = TypeChecker.fromUrl(
/// };
/// @override
/// Map<String, dynamic>? get parameters {
/// registerResponse<LoginData>(LoginData.fromJson);
/// registerResponse<LoginResponse>(_$LoginResponseFromJson);
/// return super.parameters;
/// }
/// }
@@ -94,9 +105,13 @@ class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
'post',
);
// 读取 responseType用于泛型绑定 + 自动注册 fromJson
final responseType = annotation.read('responseType').typeValue;
final responseTypeName = responseType.getDisplayString();
// 读取 responseType可选null 表示 void 响应,无需注册 fromJson
final responseTypePeek = annotation.peek('responseType');
final bool hasResponseType =
responseTypePeek != null && !responseTypePeek.isNull;
final String responseTypeName = hasResponseType
? responseTypePeek.typeValue.getDisplayString()
: 'void';
// 读取 ApiRequestType 枚举值
final requestTypeName = _readEnumName(
@@ -116,6 +131,14 @@ class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
// 从类的声明字段生成 toJson()
final toJsonBody = _buildToJsonBody(element, className);
// 有响应类型parameters getter 中注册 fromJson使用生成的私有函数
// ApiResponseGenerator 在同一 .g.dart 中生成 _$XFromJson同 library 可访问
// 无响应类型void跳过注册直接返回 super.parameters
final parametersBody = hasResponseType
? ''' registerResponse<$responseTypeName>(_\$${responseTypeName}FromJson);
return super.parameters;'''
: ' return super.parameters;';
return '''
/// Generated by @ApiRequest for [$className]
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
@@ -131,8 +154,7 @@ mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
Map<String, dynamic> toJson() => $toJsonBody;
@override
Map<String, dynamic>? get parameters {
registerResponse<$responseTypeName>($responseTypeName.fromJson);
return super.parameters;
$parametersBody
}
}
''';

View File

@@ -0,0 +1,205 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:source_gen/source_gen.dart';
import 'package:build/build.dart';
/// @JsonKey 检测器(与 ApiRequestGenerator 共用相同策略)
const _jsonKeyChecker = TypeChecker.fromUrl(
'package:json_annotation/src/json_key.dart#JsonKey',
);
/// @ApiRequest 检测器(复用,用于找到所有 responseType 入口)
const _apiRequestChecker = TypeChecker.fromUrl(
'package:networks_sdk/src/annotations/api_request.dart#ApiRequest',
);
/// Response 反序列化代码生成器
///
/// 从同一 library 中的 `@ApiRequest(responseType: T)` 声明出发,
/// 自动生成 `_$TFromJson` 私有函数,无需在 Response 类上添加任何注解。
///
/// ## 设计原理
///
/// App 层只需标注 Request 类:
/// ```dart
/// @ApiRequest(path: '...', responseType: LoginResponse)
/// class LoginRequest extends ApiRequestable<LoginResponse>
/// with _$LoginRequestApi { ... }
/// ```
///
/// 生成器从 `responseType` 出发,递归找到所有嵌套自定义类型,
/// 在同一 `.g.dart` 中生成所有 `_$XFromJson` 私有函数。
///
/// Response 类体本身是纯 Dart 类,**无需任何注解,无需 factory fromJson**
/// ```dart
/// class LoginResponse {
/// @JsonKey(name: 'access_token')
/// final String accessToken;
/// final LoginProfile profile;
/// // 完毕fromJson 由生成器自动提供
/// }
/// ```
///
/// ## 生成示例
///
/// ```dart
/// LoginProfile _$LoginProfileFromJson(Map<String, dynamic> json) {
/// return LoginProfile(
/// uid: json['uid'] as int,
/// uuid: json['uuid'] as String,
/// );
/// }
///
/// LoginResponse _$LoginResponseFromJson(Map<String, dynamic> json) {
/// return LoginResponse(
/// accessToken: json['access_token'] as String,
/// profile: _$LoginProfileFromJson(json['profile'] as Map<String, dynamic>),
/// );
/// }
/// ```
///
/// ## 支持的字段类型
///
/// - 基础类型String / int / bool / double / num
/// - 可空类型String? / int? / 自定义类?
/// - 嵌套对象(自动递归,在同一 `.g.dart` 中生成被依赖类的函数)
/// - `@JsonKey(name: '...')` 字段重命名
/// - `@JsonKey(includeFromJson: false)` 跳过字段
class ApiResponseGenerator extends Generator {
@override
String generate(LibraryReader library, BuildStep buildStep) {
final buffer = StringBuffer();
// 跟踪已生成的类,避免同一 library 中多个 Request 引用同一 Response 类时重复生成
final generated = <String>{};
for (final annotated in library.annotatedWith(_apiRequestChecker)) {
final annotation = annotated.annotation;
final responseTypePeek = annotation.peek('responseType');
if (responseTypePeek == null || responseTypePeek.isNull) continue;
final responseType = responseTypePeek.typeValue;
if (responseType is! InterfaceType) continue;
final responseClass = responseType.element;
if (responseClass is! ClassElement) continue;
// 只生成属于当前 library 的类(跨包类型跳过)
if (responseClass.library != library.element) continue;
_generateRecursive(responseClass, library, generated, buffer);
}
return buffer.toString();
}
/// 递归生成:先生成被依赖的嵌套类型,再生成当前类型(保证引用顺序)
void _generateRecursive(
ClassElement element,
LibraryReader library,
Set<String> generated,
StringBuffer buffer,
) {
final typeName = element.name!;
if (generated.contains(typeName)) return;
generated.add(typeName);
// 先递归处理嵌套的自定义类型
// InterfaceType 的 element 在 nullable 和非 nullable 情况下指向同一个 ClassElement
for (final field in element.fields.where(
(f) => !f.isStatic && !f.isSynthetic,
)) {
final fieldType = field.type;
if (fieldType is InterfaceType) {
final fieldClass = fieldType.element;
if (fieldClass is ClassElement &&
fieldClass.library == library.element &&
!_isPrimitive(fieldClass.name!)) {
_generateRecursive(fieldClass, library, generated, buffer);
}
}
}
final params = _buildConstructorParams(element);
buffer.write('''
/// Generated by ApiResponseGenerator for [$typeName]
$typeName _\$${typeName}FromJson(Map<String, dynamic> json) {
return $typeName(
$params );
}
''');
}
/// 判断是否为不需要递归生成的基础类型
bool _isPrimitive(String typeName) {
const primitives = {
'String',
'int',
'double',
'bool',
'num',
'dynamic',
'Object',
'List',
'Map',
'Set',
'Iterable',
};
return primitives.contains(typeName);
}
/// 从类的字段生成构造函数参数列表
String _buildConstructorParams(ClassElement element) {
final fields = element.fields
.where((f) => !f.isStatic && !f.isSynthetic)
.toList();
final lines = <String>[];
for (final field in fields) {
final jsonKeyAnnotation = _jsonKeyChecker.firstAnnotationOfExact(field);
// @JsonKey(includeFromJson: false) → 跳过
final includeFromJson = jsonKeyAnnotation
?.getField('includeFromJson')
?.toBoolValue();
if (includeFromJson == false) continue;
final fieldName = field.name!;
final jsonName =
jsonKeyAnnotation?.getField('name')?.toStringValue() ?? fieldName;
final type = field.type;
final isNullable = type.nullabilitySuffix == NullabilitySuffix.question;
final expr = _castExpression(type, jsonName, isNullable);
lines.add(' $fieldName: $expr,');
}
return lines.join('\n');
}
/// 根据字段类型生成 JSON 取值表达式
String _castExpression(DartType type, String jsonKey, bool isNullable) {
final q = isNullable ? '?' : '';
final access = "json['$jsonKey']";
// 基础类型:直接 as 转换
if (type.isDartCoreString) return '$access as String$q';
if (type.isDartCoreInt) return '$access as int$q';
if (type.isDartCoreBool) return '$access as bool$q';
if (type.isDartCoreDouble) return '$access as double$q';
if (type.isDartCoreNum) return '$access as num$q';
// 嵌套对象:调用同一 part 文件中生成的 _$TypeFromJson 私有函数
if (type is InterfaceType) {
final typeName = type.element.name!;
if (isNullable) {
return '$access == null ? null : _\$${typeName}FromJson($access as Map<String, dynamic>)';
}
return '_\$${typeName}FromJson($access as Map<String, dynamic>)';
}
// 兜底
return '$access as dynamic';
}
}

View File

@@ -2,11 +2,16 @@ import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'api_request_generator.dart';
import 'api_response_generator.dart';
/// @ApiRequest 代码生成器入口
/// @ApiRequest / @ApiResponse 代码生成器入口
///
/// 在 `build.yaml` 中注册此 builder配合 `build_runner` 使用。
/// 生成的代码通过 `SharedPartBuilder` 合并到 `.g.dart` 文件中
/// 与 `json_serializable` 等生成器共存。
Builder apiRequestBuilder(BuilderOptions options) =>
SharedPartBuilder([ApiRequestGenerator()], 'api_request');
/// 生成的代码通过 `SharedPartBuilder` 合并到 `.g.dart` 文件中
///
/// - `ApiRequestGenerator`:为 `@ApiRequest` 生成 Request mixintoJson + path/method 等)
/// - `ApiResponseGenerator`:从 `@ApiRequest(responseType: T)` 推导,生成 Response 的所有 `_$XFromJson` 函数
Builder apiRequestBuilder(BuilderOptions options) => SharedPartBuilder([
ApiRequestGenerator(),
ApiResponseGenerator(),
], 'api_request');