مستندات API (Swagger / OpenAPI)
فینچ یک سیستم مستندسازی OpenAPI داخلی دارد. هر مسیر API را با استفاده از ApiDoc توصیف میکنید و فینچ یک OpenAPI 3.1 JSON spec قابلخوانش برای ماشین و یک Swagger UI تولید میکند که به توسعهدهندهها امکان میدهد API شما را از داخل مرورگر بررسی و تست کنند.
راهاندازی مستندات API به سه مرحله نیاز دارد:
- یک نمونه از
ApiControllerبسازید. - مسیرهای مستندسازی را ثبت کنید (خروجی JSON اوپنایپیآی و Swagger UI).
- اشیاء
ApiDocرا به مسیرهایی که باید در مستندات نمایش داده شوند متصل کنید.
مرحله ۱ — ساخت ApiController
ApiController تمام مسیرهای اپلیکیشن شما را میخواند و OpenAPI spec را تولید میکند:
final apiController = ApiController(
title: 'My App API',
app: app,
security: 'apiKey', // اختیاری — نامی که برای طرح امنیتی bearer نمایش داده میشود، پیشفرض 'apiKey'
);
مرحله ۲ — ثبت مسیرهای مستندسازی
دو مسیر به router خود اضافه کنید: یکی برای خروجی خام JSON اوپنایپیآی و دیگری برای Swagger UI:
[
// OpenAPI JSON spec را برمیگرداند — توسط Swagger UI و کلاینتهای API استفاده میشود
FinchRoute(
key: 'root.api.docs',
path: 'api/docs',
index: apiController.indexPublic,
),
// صفحه Swagger UI — آدرس JSON spec را پاس بدهید
FinchRoute(
key: 'root.swagger',
path: 'swagger',
index: () => apiController.swagger(rq.url('api/docs')),
),
];
مسیر /swagger را در مرورگر خود باز کنید تا مستندات تعاملی را ببینید.
بهصورت پیشفرض، Swagger UI (و همچنین endpoint ساده
index()که JSON برمیگرداند) فقط در حالت local debug (isLocalDebug: true) در دسترس است.indexPublic()یک میانبر برایindex(showPublic: true)است، بنابراین مسیر JSON spec در بالا ذاتاً عمومی است — برای اینکهapiController.swagger(...)نیز صرفنظر از حالت debug عمومی شود، مقدارshowPublic: trueرا بهصراحت به آن پاس دهید.
فقط مسیرهایی که path آنها با /api شروع میشود در spec تولیدشده گنجانده میشوند، و فقط در صورتی که یک apiDoc به آنها متصل شده باشد.
مرحله ۳ — تعریف ApiDoc برای مسیرها
هر مسیر میتواند یک ApiDoc داشته باشد که توضیح میدهد آن مسیر چه کاری انجام میدهد، چه پارامترها/بدنهای میپذیرد، و چه پاسخهایی برمیگرداند. آن را از طریق ویژگی apiDoc به یک FinchRoute متصل کنید.
FinchRoute(
key: 'api.books.list',
path: 'api/books',
methods: Methods.ONLY_GET,
index: booksController.list,
apiDoc: ApiDoc(
get: ApiDoc(
description: 'Returns a paginated list of books.',
parameters: [
ApiParameter<int>(
'page',
isRequired: false,
paramIn: ParamIn.query,
def: 1,
),
ApiParameter<int>(
'limit',
isRequired: false,
paramIn: ParamIn.query,
def: 20,
),
],
response: {
'200': [
ApiResponse<int>('count', def: 0),
ApiResponse<List>('rows', def: []),
],
'404': ApiDocuments.r404,
},
),
),
),
ویژگیهای ApiDoc
ApiDoc را میتوان هم در سطح مسیر و هم برای هر متد HTTP جداگانه بهکار برد. برای هر متد یک ApiDoc داخلی را nest کنید — parameters/body/response در سطح مسیر روی همه متدها اعمال میشود، و نسخه مخصوص هر متد (get/post/put/delete) هنگام ساخت spec توسط فینچ روی آن merge میشود:
ApiDoc(
get: ApiDoc(description: '...', parameters: [...], response: {...}),
post: ApiDoc(description: '...', parameters: [...], body: [...], response: {...}),
put: ApiDoc(description: '...', parameters: [...], body: [...], response: {...}),
delete: ApiDoc(description: '...', parameters: [...], response: {...}),
)
ApiParameter
ApiParameter<T> یک پارامتر ورودی را توصیف میکند که بخشی از بدنه درخواست نیست — بخشهای path، رشتههای query، هدرها یا کوکیها:
| ویژگی | نوع | توضیح |
|---|---|---|
| آرگومان اول | String |
نام پارامتر |
isRequired |
bool |
آیا این فیلد اجباری است (پیشفرض false) |
paramIn |
ParamIn |
مقدار از کجا میآید (پیشفرض ParamIn.query) |
def |
T? |
مقدار پیشفرض/نمونه که در Swagger UI نمایش داده میشود |
description |
String? |
توضیح اختیاری که در Swagger UI نمایش داده میشود |
مقادیر ParamIn — چیزی به نام ParamIn.body وجود ندارد؛ در عوض فیلدهای بدنه درخواست را با ApiBodyField مستند کنید (به ادامه مراجعه کنید):
| مقدار | توضیح |
|---|---|
ParamIn.path |
بخش path در URL (مثال: /books/{id}) |
ParamIn.query |
رشته query (مثال: ?page=1) |
ParamIn.header |
هدر درخواست HTTP |
ParamIn.cookie |
کوکی درخواست |
ApiBodyField — مستندسازی بدنه درخواست
برای مسیرهای POST/PUT، بدنه JSON درخواست را با ApiBodyField<T> در لیست body مربوط به ApiDoc توصیف کنید — نه با ApiParameter/ParamIn:
ApiDoc(
body: [
ApiBodyField<String>('title', isRequired: true, description: 'Book title'),
ApiBodyField<String>('author', isRequired: true),
],
response: {
'200': [ApiResponse<bool>('success', def: true)],
},
)
ApiResponse
ApiResponse<T> یک فیلد در بدنه پاسخ را توصیف میکند:
ApiResponse<String>('title', def: 'Book Title'),
ApiResponse<int>('id', def: 0),
ApiResponse<bool>('success', def: true),
ApiResponse<Map<String, dynamic>>('data', def: {}),
اشکال پاسخ قابلاستفادهمجدد
فینچ هیچ ثابت آمادهای به سبک r_401/r_404/r_500 ارائه نمیدهد — این نامگذاری صرفاً یک قرارداد در پروژه نمونه (example project) است، نه یک ویژگی از خودِ framework. یکبار ثابتهای List<ApiResponse> مخصوص به خودتان را تعریف کنید و آنها را در چندین مسیر استفاده مجدد کنید تا از تکرار اشکال رایج خطاها جلوگیری شود:
class ApiDocuments {
static final r404 = [
ApiResponse<int>('timestamp_start', def: 0),
ApiResponse<String>('message', def: 'Not found'),
ApiResponse<bool>('success', def: false),
ApiResponse<int>('status', def: 404),
];
static final r403 = [
ApiResponse<int>('timestamp_start', def: 0),
ApiResponse<String>('message', def: 'Please login'),
ApiResponse<bool>('success', def: false),
ApiResponse<int>('status', def: 403),
];
}
// در نگاشت response خود استفاده کنید:
response: {
'200': [...],
'403': ApiDocuments.r403,
'404': ApiDocuments.r404,
}
نمونه کامل یک مسیر
FinchRoute(
key: 'api.books.one',
path: 'api/books/{id}',
methods: Methods.GET_POST,
index: booksController.one,
apiDoc: ApiDoc(
get: ApiDoc(
description: 'Get a single book by ID.',
parameters: [
ApiParameter<String>('id', isRequired: true, paramIn: ParamIn.path),
],
response: {
'200': [
ApiResponse<int>('id', def: 0),
ApiResponse<String>('title', def: ''),
ApiResponse<String>('author', def: ''),
],
'404': ApiDocuments.r404,
},
),
post: ApiDoc(
description: 'Update a book by ID.',
parameters: [
ApiParameter<String>('id', isRequired: true, paramIn: ParamIn.path),
],
body: [
ApiBodyField<String>('title', isRequired: false),
ApiBodyField<String>('author', isRequired: false),
],
response: {
'200': [ApiResponse<bool>('success', def: true)],
'404': ApiDocuments.r404,
},
),
),
),