极简接口定义和响应定义,支持更多的解析器
This commit is contained in:
@@ -29,3 +29,4 @@ export 'src/domain/entities/socket_error.dart';
|
||||
|
||||
// Annotations(代码生成)
|
||||
export 'src/annotations/api_request.dart';
|
||||
export 'src/annotations/api_response.dart';
|
||||
|
||||
@@ -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,
|
||||
|
||||
31
packages/networks_sdk/lib/src/annotations/api_response.dart
Normal file
31
packages/networks_sdk/lib/src/annotations/api_response.dart
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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 mixin(toJson + path/method 等)
|
||||
/// - `ApiResponseGenerator`:从 `@ApiRequest(responseType: T)` 推导,生成 Response 的所有 `_$XFromJson` 函数
|
||||
Builder apiRequestBuilder(BuilderOptions options) => SharedPartBuilder([
|
||||
ApiRequestGenerator(),
|
||||
ApiResponseGenerator(),
|
||||
], 'api_request');
|
||||
|
||||
Reference in New Issue
Block a user