كيفية استخدام Spectral مع TypeScript

Audrey Lopez

Audrey Lopez

19 مايو 2025

كيفية استخدام Spectral مع TypeScript

يُعد الحفاظ على الاتساق والجودة والالتزام بمعايير التصميم أمرًا بالغ الأهمية للفرق التي تستخدم واجهات برمجة التطبيقات (APIs). توفر مواصفات API مثل OpenAPI و AsyncAPI مخططًا تفصيليًا، ولكن ضمان اتباع هذه المخططات بشكل صحيح عبر العديد من الخدمات والفرق يمكن أن يكون تحديًا كبيرًا. هنا يأتي دور أدوات تدقيق API (API linting tools)، وتبرز Spectral كخيار مرن وقوي ومفتوح المصدر. عند دمجها مع TypeScript، تمكّن Spectral المطورين من إنشاء قواعد مخصصة قوية وآمنة من حيث النوع (type-safe)، مما يرتقي بإدارة API إلى مستوى جديد.

سيرشدك هذا البرنامج التعليمي خلال عملية الاستفادة من Spectral مع TypeScript، بدءًا من الإعداد الأولي وحتى صياغة منطق تحقق مخصص معقد. سنستكشف كيف يعزز TypeScript تطوير قواعد Spectral، مما يؤدي إلى حلول تدقيق API أكثر قابلية للصيانة وموثوقية.

💡
هل تريد أداة رائعة لاختبار API تقوم بإنشاء توثيق API جميل؟

هل تريد منصة متكاملة وشاملة لفريق المطورين لديك للعمل معًا بأقصى قدر من الإنتاجية؟

يلبي Apidog جميع متطلباتك، ويحل محل Postman بسعر أقل بكثير!
button

فهم Spectral: حارس مواصفات API الخاصة بك

قبل الغوص في تكامل TypeScript، دعنا نوضح ما هي Spectral ولماذا هي أداة قيمة في مجموعة أدوات تطوير API الخاصة بك.

Spectral هي أداة تدقيق (linter) مفتوحة المصدر لملفات JSON/YAML مع تركيز أساسي على تنسيقات وصف API مثل OpenAPI (v2 و v3) و AsyncAPI. هدفها هو المساعدة في فرض إرشادات تصميم API، واكتشاف الأخطاء الشائعة، وضمان الاتساق عبر بيئة API الخاصة بك. فكر فيها على أنها ESLint أو TSLint، ولكن خصيصًا لعقود API الخاصة بك.

الفوائد الرئيسية لاستخدام Spectral:

تعمل Spectral بناءً على مجموعات القواعد (rulesets). مجموعة القواعد هي مجموعة من القواعد، حيث تستهدف كل قاعدة أجزاء محددة من مستند API الخاص بك (باستخدام تعبيرات JSONPath) وتطبق منطق التحقق. تأتي Spectral مع مجموعات قواعد مدمجة (مثل spectral:oas لمعايير OpenAPI)، ولكن قوتها الحقيقية تكمن في القدرة على تحديد مجموعات قواعد مخصصة.

لماذا TypeScript لقواعد Spectral المخصصة؟

بينما يمكن تعريف مجموعات قواعد Spectral في YAML أو JavaScript (ملفات .js)، فإن استخدام TypeScript لتطوير الوظائف المخصصة يوفر مزايا كبيرة:

من خلال كتابة وظائف Spectral المخصصة الخاصة بك في TypeScript، فإنك تجلب نفس الصرامة وفوائد الأدوات إلى كود إدارة API الخاص بك كما تفعل مع كود تطبيقك.

إعداد بيئة Spectral و TypeScript الخاصة بك

دعنا نبدأ العمل العملي ونعد الأدوات اللازمة.

المتطلبات الأساسية:

خطوات التثبيت:

أولاً، ستحتاج إلى واجهة سطر الأوامر Spectral (Spectral CLI) لتشغيل عمليات التدقيق واختبار قواعدك. غالبًا ما يكون من المفيد تثبيتها عالميًا أو استخدام npx.Bash

npm install -g @stoplight/spectral-cli
# أو
yarn global add @stoplight/spectral-cli

لتطوير قواعد مخصصة برمجياً واستخدام مكتبات Spectral الأساسية داخل مشروع TypeScript، قم بتثبيت حزم Spectral اللازمة:Bash

npm install @stoplight/spectral-core @stoplight/spectral-functions @stoplight/spectral-rulesets typescript ts-node --save-dev
# أو
yarn add @stoplight/spectral-core @stoplight/spectral-functions @stoplight/spectral-rulesets typescript ts-node --dev

دعنا نفصّل هذه الحزم:

تهيئة TypeScript:

أنشئ ملف tsconfig.json في جذر مشروعك إذا لم يكن لديك واحد بالفعل. قد تبدو التهيئة الأساسية كما يلي:JSON

{
  "compilerOptions": {
    "target": "es2020", // أو إصدار أحدث
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist", // دليل الإخراج لملفات JavaScript المترجمة
    "rootDir": "./src", // دليل المصدر لملفات TypeScript الخاصة بك
    "resolveJsonModule": true // يسمح باستيراد ملفات JSON
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

اضبط outDir و rootDir وفقًا لهيكل مشروعك. سنفترض أن وظائف TypeScript المخصصة الخاصة بك ستقع في الدليل src.

مفاهيم Spectral الأساسية: القواعد، مجموعات القواعد، والوظائف

قبل كتابة وظائف TypeScript، دعنا نعزز فهمنا للمكونات الرئيسية لـ Spectral.

القواعد (Rules):

تُعرّف القاعدة فحصًا محددًا يجب إجراؤه. تشمل الخصائص الرئيسية للقاعدة ما يلي:

مجموعات القواعد (Rulesets):

مجموعة القواعد هي ملف YAML أو JavaScript (مثل .spectral.yaml، .spectral.js، أو .spectral.ts عند الترجمة) يجمع القواعد. يمكنها أيضًا:

الوظائف (Functions):

الوظائف هي وحدات المنطق الأساسية التي تقوم بالتحقق الفعلي. توفر Spectral العديد من الوظائف المدمجة مثل:

القوة الحقيقية تأتي عندما لا تكون هذه الوظائف كافية، وتحتاج إلى كتابة وظائف مخصصة خاصة بك - وهنا يبرز TypeScript.

صياغة أول وظيفة Spectral مخصصة لك في TypeScript

دعنا ننشئ وظيفة مخصصة بسيطة. تخيل أننا نريد فرض قاعدة تقضي بأن تكون جميع ملخصات عمليات API مكتوبة بصيغة العنوان (title-cased) ولا تتجاوز 70 حرفًا.

الخطوة 1: تعريف الوظيفة المخصصة في TypeScript

أنشئ ملفًا، على سبيل المثال src/customFunctions.ts:TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';

interface TitleCaseLengthOptions {
  maxLength: number;
}

// وظيفة مخصصة للتحقق مما إذا كان النص بصيغة العنوان وضمن الحد الأقصى للطول
export const titleCaseAndLength: IFunction<string, TitleCaseLengthOptions> = (
  targetVal,
  options,
  context
): IFunctionResult[] | void => {
  const results: IFunctionResult[] = [];

  if (typeof targetVal !== 'string') {
    // لا ينبغي أن يحدث هذا إذا كان مسار 'given' يشير إلى نص، لكنها ممارسة جيدة
    return [{ message: `القيمة في المسار '${context.path.join('.')}' يجب أن تكون نصًا.` }];
  }

  // التحقق من صيغة العنوان (تحقق بسيط: الحرف الأول من كل كلمة هو حرف كبير)
  const words = targetVal.split(' ');
  const isTitleCase = words.every(word => word.length === 0 || (word[0] === word[0].toUpperCase() && (word.length === 1 || word.substring(1) === word.substring(1).toLowerCase())));

  if (!isTitleCase) {
    results.push({
      message: `الملخص "${targetVal}" في المسار '${context.path.join('.')}' يجب أن يكون بصيغة العنوان.`,
      path: [...context.path], // المسار إلى العنصر المخالف
    });
  }

  // التحقق من الطول
  const maxLength = options?.maxLength || 70; // الافتراضي 70 إذا لم يتم توفيرها
  if (targetVal.length > maxLength) {
    results.push({
      message: `الملخص "${targetVal}" في المسار '${context.path.join('.')}' يتجاوز الحد الأقصى للطول وهو ${maxLength} حرفًا. الطول الحالي: ${targetVal.length}.`,
      path: [...context.path],
    });
  }

  return results;
};

شرح:

الخطوة 2: ترجمة وظيفة TypeScript

إذا كنت تخطط لاستخدام هذه الوظيفة مع مجموعة قواعد .spectral.yaml أو .spectral.js تشير إلى functionsDir، فستحتاج إلى ترجمة TypeScript إلى JavaScript.

أضف سكريبت بناء إلى ملف package.json الخاص بك:JSON

{
  "scripts": {
    "build": "tsc"
  }
}

شغّل npm run build أو yarn build. سيؤدي هذا إلى ترجمة src/customFunctions.ts إلى dist/customFunctions.js (بناءً على ملف tsconfig.json الخاص بنا).

الخطوة 3: إنشاء ملف مجموعة القواعد

دعنا ننشئ مجموعة قواعد، على سبيل المثال، .spectral.js (أو .spectral.ts إذا كنت تفضل إعدادًا يعتمد بالكامل على TypeScript، انظر القسم التالي).

إذا كنت تستخدم ملف مجموعة قواعد JavaScript يشير مباشرة إلى الوظائف المترجمة:JavaScript

// .spectral.js
const { titleCaseAndLength } = require('./dist/customFunctions'); // المسار إلى وظائف JS المترجمة

module.exports = {
  extends: [['@stoplight/spectral-rulesets/dist/rulesets/oas', 'recommended']],
  rules: {
    'operation-summary-title-case-length': {
      description: 'يجب أن تكون ملخصات العمليات بصيغة العنوان ولا تتجاوز 70 حرفًا.',
      message: '{{error}}', // الرسالة ستأتي من الوظيفة المخصصة
      given: '$.paths[*][*].summary', // تستهدف جميع ملخصات العمليات
      severity: 'warn',
      formats: ["oas3"], // تطبق فقط على مستندات OpenAPI v3
      then: {
        function: titleCaseAndLength, // تشير مباشرة إلى الوظيفة المستوردة
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    // يمكنك إضافة المزيد من القواعد هنا
  },
};

بدلاً من ذلك، إذا كنت تستخدم .spectral.yaml و functionsDir:

أولاً، تأكد من أن دليل dist يحتوي على ملف index.js يقوم بتصدير وظائفك، أو أن ملف customFunctions.js الخاص بك يقوم بتصديرها مباشرة. على سبيل المثال، إذا كان dist/customFunctions.js يحتوي على exports.titleCaseAndLength = ...;، يمكنك القيام بما يلي:YAML

# .spectral.yaml
extends:
  - ["@stoplight/spectral-rulesets/dist/rulesets/oas", "recommended"]
functionsDir: "./dist" # الدليل الذي يحتوي على وظائف JS المخصصة المترجمة
rules:
  operation-summary-title-case-length:
    description: "يجب أن تكون ملخصات العمليات بصيغة العنوان ولا تتجاوز 70 حرفًا."
    message: "{{error}}"
    given: "$.paths[*][*].summary"
    severity: "warn"
    formats: ["oas3"]
    then:
      function: customFunctions#titleCaseAndLength # بافتراض تصدير الوظائف من customFunctions.js
      functionOptions:
        maxLength: 70

هنا، تخبر customFunctions#titleCaseAndLength أداة Spectral بالبحث عن ملف customFunctions.js (أو customFunctions/index.js) في functionsDir واستخدام الوظيفة المصدرة titleCaseAndLength.

الخطوة 4: إنشاء مستند OpenAPI نموذجي

دعنا ننشئ ملف openapi.yaml بسيطًا لاختبار قاعدتنا:YAML

# openapi.yaml
openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /items:
    get:
      summary: retrieves all items from the store # غير صحيح: ليس بصيغة العنوان
      responses:
        '200':
          description: A list of items.
    post:
      summary: Adds A New Item To The Ever Expanding Collection Of Items In The Store # غير صحيح: طويل جدًا
      responses:
        '201':
          description: Item created.
  /users:
    get:
      summary: Get User Details # صحيح
      responses:
        '200':
          description: User details.

الخطوة 5: تشغيل Spectral

الآن، قم بتنفيذ واجهة سطر الأوامر Spectral مقابل مستند OpenAPI الخاص بك:Bash

spectral lint openapi.yaml --ruleset .spectral.js
# أو إذا كنت تستخدم مجموعة قواعد YAML (غالبًا ما يتم اكتشافها تلقائيًا إذا كان اسمها .spectral.yaml)
spectral lint openapi.yaml

الإخراج المتوقع:

يجب أن ترى تحذيرات مشابهة لهذا:

openapi.yaml
 2:10  warning  operation-summary-title-case-length  Summary "retrieves all items from the store" at path 'paths./items.get.summary' must be in Title Case.   paths./items.get.summary
 6:10  warning  operation-summary-title-case-length  Summary "Adds A New Item To The Ever Expanding Collection Of Items In The Store" at path 'paths./items.post.summary' exceeds maximum length of 70 characters. Current length: 78.  paths./items.post.summary

✖ 2 problems (0 errors, 2 warnings, 0 infos, 0 hints)

يؤكد هذا الإخراج أن وظيفة TypeScript المخصصة الخاصة بنا، المترجمة إلى JavaScript، تحدد الانتهاكات بشكل صحيح.

الاستخدام البرمجي لـ Spectral مع مجموعات قواعد TypeScript

للسيناريوهات الأكثر تعقيدًا أو التكامل الأوثق في التطبيقات، قد ترغب في استخدام Spectral برمجياً وتحديد مجموعة القواعد الخاصة بك بالكامل في TypeScript. هذا يتجاوز الحاجة إلى ملف .spectral.yaml أو .spectral.js منفصل إذا رغبت في ذلك، ويسمح بإنشاء مجموعات قواعد ديناميكية.

أنشئ ملف TypeScript لأداة التدقيق الخاصة بك، على سبيل المثال src/linter.ts:TypeScript

import { Spectral, Document } from '@stoplight/spectral-core';
import { oas } from '@stoplight/spectral-rulesets';
import { truthy } from '@stoplight/spectral-functions'; // مثال لوظيفة مدمجة
import { titleCaseAndLength } from './customFunctions'; // وظيفة TS المخصصة الخاصة بك
import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
import * as fs from 'fs/promises';
import * as path from 'path';

// تعريف مثيل Spectral
const spectral = new Spectral();

// تحميل مجموعات القواعد والوظائف المدمجة
spectral.setRuleset({
  extends: [[oas, 'recommended']], // يوسع قواعد OpenAPI الموصى بها
  rules: {
    'operation-summary-title-case-length': {
      description: 'يجب أن تكون ملخصات العمليات بصيغة العنوان ولا تتجاوز 70 حرفًا.',
      message: '{{error}}',
      given: '$.paths[*][*].summary',
      severity: 'warn',
      formats: ["oas3"],
      then: {
        function: titleCaseAndLength, // استخدام وظيفة TypeScript مباشرة
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    'info-contact-defined': { // مثال باستخدام وظيفة مدمجة
        description: 'يجب أن يحتوي كائن Info على كائن contact.',
        message: 'معلومات اتصال API مفقودة.',
        given: '$.info',
        severity: 'warn',
        formats: ["oas3"],
        then: {
          field: 'contact',
          function: truthy, // استخدام وظيفة مدمجة
        },
      },
  },
});

// وظيفة لتدقيق مستند
export async function lintDocument(filePath: string): Promise<ISpectralDiagnostic[]> {
  try {
    const absolutePath = path.resolve(filePath);
    const fileContent = await fs.readFile(absolutePath, 'utf-8');
    
    // إنشاء كائن Spectral Document
    // الوسيط الثاني (URI) مهم لحل المراجع النسبية ($refs) إن وجدت
    const document = new Document(fileContent, undefined, absolutePath); 
    
    const results = await spectral.run(document);
    return results;
  } catch (error) {
    console.error('خطأ في تدقيق المستند:', error);
    return [];
  }
}

// مثال للاستخدام (مثل في سكريبت أو وحدة أخرى)
async function main() {
  const diagnostics = await lintDocument('openapi.yaml'); // المسار إلى مواصفات API الخاصة بك
  if (diagnostics.length > 0) {
    console.log('تم العثور على مشاكل في تدقيق API:');
    diagnostics.forEach(issue => {
      console.log(
        `- [${issue.severity === 0 ? 'خطأ' : issue.severity === 1 ? 'تحذير' : issue.severity === 2 ? 'معلومة' : 'تلميح'}] ${issue.code} (${issue.message}) في ${issue.path.join('.')}`
      );
    });
  } else {
    console.log('لم يتم العثور على مشاكل في تدقيق API. عمل رائع!');
  }
}

// إذا كنت ترغب في تشغيل هذا الملف مباشرة باستخدام ts-node
if (require.main === module) {
  main().catch(console.error);
}

لتشغيل هذا:Bash

npx ts-node src/linter.ts

يوفر هذا النهج أقصى قدر من المرونة:

تقنيات متقدمة للوظائف المخصصة

دعنا نستكشف بعض الجوانب الأكثر تقدمًا لإنشاء وظائف مخصصة.

الوظائف المخصصة غير المتزامنة (Asynchronous Custom Functions):

إذا كانت وظيفتك المخصصة تحتاج إلى إجراء عمليات غير متزامنة (مثل جلب مورد خارجي للتحقق منه، على الرغم من استخدام ذلك بحذر للأداء)، يمكنك تعريفها كوظيفة async.TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';

export const checkExternalResource: IFunction<string, { url: string }> = 
  async (targetVal, options, context): Promise<IFunctionResult[] | void> => {
  try {
    const response = await fetch(`${options.url}/${targetVal}`);
    if (!response.ok) {
      return [{ message: `المورد '${targetVal}' غير موجود في ${options.url}. الحالة: ${response.status}` }];
    }
  } catch (error: any) {
    return [{ message: `خطأ في جلب المورد '${targetVal}': ${error.message}` }];
  }
};

ستقوم Spectral بشكل صحيح بـ await وظيفتك غير المتزامنة.

الوصول إلى المستند الكامل والقيم المحلولة (Resolved Values):

يوفر كائن context.document الوصول إلى المستند بأكمله بعد التحليل. والأكثر قوة، يوفر context.document.resolved الإصدار الكامل الذي تم إلغاء الإشارة إليه (dereferenced) من المستند، وهو أمر ضروري عند التعامل مع مؤشرات $ref.TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
import {isPlainObject} from '@stoplight/json';

// مثال: التأكد من أن مخططًا مشار إليه يحتوي على خاصية محددة
export const referencedSchemaHasProperty: IFunction<{$ref: string}, { propertyName: string }> = (
  targetVal, // سيكون هذا الكائن مثل { $ref: '#/components/schemas/MySchema' }
  options,
  context
): IFunctionResult[] | void => {
  if (!targetVal.$ref) return;

  // يمكن لأسلوب `context.document.resolveAnchor` العثور على القيمة المحلولة لـ $ref
  const resolvedValue = context.document.resolveAnchor(targetVal.$ref);

  if (!resolvedValue || !isPlainObject(resolvedValue.value)) {
    return [{ message: `تعذر حل $ref: ${targetVal.$ref}` }];
  }
  
  const schemaProperties = resolvedValue.value.properties as Record<string, unknown> | undefined;

  if (!schemaProperties || schemaProperties[options.propertyName] === undefined) {
    return [{
      message: `المخطط المشار إليه بواسطة "${targetVal.$ref}" في المسار '${context.path.join('.')}' يجب أن يحتوي على الخاصية "${options.propertyName}".`,
      path: [...context.path, '$ref'] // يشير إلى $ref نفسه
    }];
  }
};

سيتم استخدام هذه الوظيفة مع مسار given يستهدف الكائنات التي تحتوي على $ref، على سبيل المثال: $.paths[*].*.responses.*.content.*.schema.

العمل مع التنسيقات:

تأكد من أن قواعدك المخصصة تحدد التنسيقات التي تنطبق عليها (مثل oas2، oas3، asyncapi2). هذا يمنع تشغيل القواعد على أنواع مستندات غير متوافقة. يمكنك الوصول إلى التنسيق المكتشف داخل وظيفتك عبر context.document.formats.

تنظيم وتوسيع مجموعات القواعد

مع نمو مجموعتك من القواعد المخصصة، يصبح التنظيم أمرًا أساسيًا.

// .spectral.js (مجموعة القواعد الرئيسية)
module.exports = {
  extends: ['./rulesets/common-rules.js', './rulesets/security-rules.js'],
  // ... قواعد أو تجاوزات أخرى
};

دمج Spectral في سير عملك

تتحقق القوة الحقيقية لتدقيق API عندما يتم أتمتتها.

# مثال لخطوة GitHub Action
- name: Lint API Specification
  run: spectral lint ./path/to/your/api-spec.yaml --ruleset ./.spectral.js --fail-severity=error

أفضل الممارسات لقواعد Spectral و TypeScript المخصصة

خاتمة: ارتقِ بإدارة API الخاصة بك باستخدام Spectral و TypeScript

توفر Spectral إطارًا قويًا لتدقيق API، ومن خلال الاستفادة من TypeScript لتطوير القواعد المخصصة، فإنك تجلب سلامة نوع محسّنة، وتجربة مطور أفضل، وقابلية صيانة لاستراتيجية إدارة API الخاصة بك. سواء كنت تفرض أفضل ممارسات OpenAPI، أو اصطلاحات تسمية خاصة بالشركة، أو منطق عمل معقدًا ضمن تصميمات API الخاصة بك، فإن الجمع بين محرك قواعد Spectral المرن ونظام كتابة الأنواع القوي في TypeScript يوفر حلاً قويًا.

من خلال دمج Spectral في دورة حياة التطوير الخاصة بك، من ملاحظات بيئة التطوير المتكاملة (IDE) إلى مسارات CI/CD، يمكنك ضمان أن واجهات API الخاصة بك متسقة، وعالية الجودة، وتلتزم بالمعايير المحددة، مما يؤدي في النهاية إلى واجهات API أكثر موثوقية وأسهل في الاستهلاك. ابدأ صغيرًا، كرر على مجموعات القواعد الخاصة بك، ومكّن فرقك بالأدوات اللازمة لبناء واجهات API أفضل.

ممارسة تصميم API في Apidog

اكتشف طريقة أسهل لبناء واستخدام واجهات برمجة التطبيقات