مستندات API (Swagger / OpenAPI)

فینچ یک سیستم مستندسازی OpenAPI داخلی دارد. هر مسیر API را با استفاده از ApiDoc توصیف می‌کنید و فینچ یک OpenAPI 3.1 JSON spec قابل‌خوانش برای ماشین و یک Swagger UI تولید می‌کند که به توسعه‌دهنده‌ها امکان می‌دهد API شما را از داخل مرورگر بررسی و تست کنند.

راه‌اندازی مستندات API به سه مرحله نیاز دارد:

  1. یک نمونه از ApiController بسازید.
  2. مسیرهای مستندسازی را ثبت کنید (خروجی JSON اوپن‌ای‌پی‌آی و Swagger UI).
  3. اشیاء 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,
      },
    ),
  ),
),