5 أشياء لم تعملها من قبل مع مواصفات REST

مواصفات REST

تعامل معظم مطوري الواجهة الأمامية والخلفية مع مواصفات REST وواجهات برمجة تطبيقات RESTful من قبل. ولكن ليست كل واجهات برمجة تطبيقات RESTful يتم إنشاؤها على قدم المساواة. في الواقع نادرًا ما يكونون مستريحين على الإطلاق …

ما هي RESTful API؟

إنها أسطورة.

إذا كنت تعتقد أن مشروعك يحتوي على RESTful API فأنت على الأرجح مخطئ. الفكرة وراء RESTful API هي التطوير بطريقة تتبع جميع القواعد والقيود المعمارية الموضحة في مواصفات REST. من الناحية الواقعية هذا مستحيل إلى حد كبير في الممارسة.

من ناحية أخرى يحتوي REST على الكثير من التعريفات الضبابية والغامضة. على سبيل المثال في الممارسة العملية يتم استخدام بعض المصطلحات من طريقة HTTP وقواميس أكواد الحالة على عكس الأغراض المقصودة منها أو لا يتم استخدامها على الإطلاق.

من ناحية أخرى فإن تطوير REST يخلق الكثير من القيود. على سبيل المثال يعد استخدام الموارد الذرية دون المستوى الأمثل لواجهات برمجة التطبيقات الواقعية المستخدمة في تطبيقات الأجهزة المحمولة. يؤدي الرفض الكامل لتخزين البيانات بين الطلبات إلى حظر آلية “user session” التي تظهر في كل مكان تقريبًا.

لكن مهلا، الأمر ليس بهذا السوء!

ما الذي تحتاجه لمواصفات REST API؟

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

عادةً ما ترتبط مواصفات REST API بوثائقها. بخلاف المواصفات – الوصف الرسمي لواجهة برمجة التطبيقات – يُقصد بالوثائق أن تكون سهلة القراءة: على سبيل المثال يقرأها مطورو تطبيقات الجوال أو تطبيقات الويب التي تستخدم واجهة برمجة التطبيقات الخاصة بك.

لا يقتصر الوصف الصحيح لواجهة برمجة التطبيقات على كتابة وثائق واجهة برمجة التطبيقات بشكل جيد. في هذه المقالة أريد أن أشارك أمثلة حول كيف يمكنك:

  • اجعل اختبارات الوحدة الخاصة بك أبسط وأكثر موثوقية
  • إعداد معالجة مدخلات المستخدم والتحقق من صحتها
  • أتمتة التسلسل وضمان اتساق الاستجابة وحتى
  • استمتع بفوائد الكتابة الثابتة.

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

OpenAPI

يعد OpenAPI حاليًا التنسيق الأكثر قبولًا على نطاق واسع لمواصفات REST API. المواصفات مكتوبة في ملف واحد بتنسيق JSON أو YAML يتكون من ثلاثة أقسام:

  • رأس مع اسم API ووصف وإصدار بالإضافة إلى أي معلومات إضافية.
  • أوصاف جميع الموارد بما في ذلك المعرفات وطرق HTTP وجميع معلمات الإدخال ورموز الاستجابة وأنواع بيانات الجسم مع روابط للتعريفات.
  • جميع التعريفات التي يمكن استخدامها للإدخال أو الإخراج بتنسيق مخطط JSON  (والذي نعم يمكن أيضًا تمثيله في YAML).

هيكل OpenAPI له عيبان هامان: إنه معقد للغاية وأحيانًا زائدة عن الحاجة. يمكن أن يكون لمشروع صغير مواصفات JSON لآلاف السطور. يصبح الاحتفاظ بهذا الملف يدويًا مستحيلًا. يعد هذا تهديدًا كبيرًا لفكرة تحديث المواصفات أثناء تطوير واجهة برمجة التطبيقات.

هناك عدة برامج تحرير تسمح لك بوصف واجهة برمجة التطبيقات  (API) وإنتاج إخراج OpenAPI. تشمل الخدمات الإضافية والحلول السحابية المستندة إليها Swagger و Apiary و Stoplight و Restlet وغيرها الكثير.

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

Tinyspec

في هذه المقالة سأستخدم أمثلة تستند إلى تنسيق تعريف REST API الخاص بي tinyspec. تتكون التعريفات من ملفات صغيرة ذات بناء جملة بديهي. يصفون نقاط النهاية ونماذج البيانات المستخدمة في المشروع. يتم تخزين الملفات بجانب الكود مما يوفر مرجعًا سريعًا والقدرة على التحرير أثناء كتابة التعليمات البرمجية. يتم تجميع Tinyspec تلقائيًا في تنسيق OpenAPI كامل يمكن استخدامه على الفور في مشروعك.

سأستخدم أيضًا أمثلة Node.js (Koa و Express) و Ruby on Rails لكن الممارسات التي سأوضحها قابلة للتطبيق على معظم التقنيات بما في ذلك Python و PHP و Java.

این تتشعشع مواصفات API؟

الآن بعد أن أصبح لدينا بعض الخلفية يمكننا استكشاف كيفية تحقيق أقصى استفادة من واجهة برمجة التطبيقات المحددة بشكل صحيح.

1. اختبارات وحدة نقطة النهاية (endpoint unit test)

يعتبر التطوير المدفوع بالسلوك  (BDD) مثاليًا لتطوير واجهات برمجة تطبيقات REST. من الأفضل كتابة اختبارات الوحدة ليس لفئات أو نماذج أو وحدات تحكم منفصلة ولكن لنقاط نهاية معينة. في كل اختبار تقوم بمحاكاة طلب HTTP حقيقي والتحقق من استجابة الخادم. بالنسبة إلى Node.js توجد حزم supertest و chai-http لمحاكاة الطلبات أما بالنسبة إلى Ruby on Rails فهناك حزم محمولة جواً.

لنفترض أن لدينا مخطط مستخدم ونقطة نهاية GET /users  التي تعرض جميع المستخدمين. إليك بعض بناء جملة tinyspec الذي يصف هذا:

# user.models.tinyspec
User {name, isAdmin: b, age?: i}

# users.endpoints.tinyspec
GET /users
    => {users: User[]}

وإليك كيفية كتابة الاختبار المقابل:

Node.js

describe('/users', () => {
  it('List all users', async () => {
    const { status, body: { users } } = request.get('/users');

    expect(status).to.equal(200);
    expect(users[0].name).to.be('string');
    expect(users[0].isAdmin).to.be('boolean');
    expect(users[0].age).to.be.oneOf(['boolean', null]);
  });
});

Ruby on Rails

describe 'GET /users' do
  it 'List all users' do
    get '/users'

    expect_status(200)
    expect_json_types('users.*', {
      name: :string,
      isAdmin: :boolean,
      age: :integer_or_null,
    })
  end 
end

عندما يكون لدينا بالفعل المواصفات التي تصف استجابات الخادم يمكننا تبسيط الاختبار والتحقق فقط مما إذا كانت الاستجابة تتبع المواصفات. يمكننا استخدام نماذج tinyspec كل منها يمكن تحويله إلى مواصفات OpenAPI التي تتبع تنسيق مخطط JSON.

يمكن التحقق من صحة أي كائن حرفي في JS (أو Hash في Ruby ​​و deb في Python والمصفوفة الترابطية في PHP وحتى الخريطة في Java) من أجل التوافق مع مخطط JSON. توجد أيضًا مكونات إضافية مناسبة لاختبار الأطر على سبيل المثال jest-ajv (npm) و chai-ajv-json-schema (npm) و json_matchers لـ RSpec (rubygem).

قبل استخدام المخططات دعنا نستوردها في المشروع. أولا، قم بإنشاء ملف openapi.json بناءً على مواصفات tinyspec  (يمكنك القيام بذلك تلقائيًا قبل كل تشغيل اختباري):

tinyspec -j -o openapi.json

Node.js

يمكنك الآن استخدام JSON الذي تم إنشاؤه في المشروع والحصول على مفتاح التعريف منه. يحتوي هذا المفتاح على جميع مخططات JSON. قد تحتوي المخططات على مراجع تبادلية $ref لذلك إذا كان لديك أي مخططات مضمنة (على سبيل المثال Blog {posts: Post []})  فأنت بحاجة إلى إلغاء تغليفها لاستخدامها في التحقق من الصحة. لهذا سوف نستخدم json-schema-deref-sync (npm).

import deref from 'json-schema-deref-sync';
const spec = require('./openapi.json');
const schemas = deref(spec).definitions;

describe('/users', () => {
  it('List all users', async () => {
    const { status, body: { users } } = request.get('/users');

    expect(status).to.equal(200);
    // Chai
    expect(users[0]).to.be.validWithSchema(schemas.User);
    // Jest
    expect(users[0]).toMatchSchema(schemas.User);
  });
});

Ruby on rails

تعرف الوحدة النمطية json_matchers كيفية التعامل مع مراجع $ ref ولكنها تتطلب ملفات مخطط منفصلة في الموقع المحدد لذلك ستحتاج إلى تقسيم ملف swagger.json إلى عدة ملفات أصغر أولاً:

# ./spec/support/json_schemas.rb
require 'json'
require 'json_matchers/rspec'

JsonMatchers.schema_root = 'spec/schemas'

# Fix for json_matchers single-file restriction
file = File.read 'spec/schemas/openapi.json'
swagger = JSON.parse(file, symbolize_names: true)
swagger[:definitions].keys.each do |key|
  File.open("spec/schemas/#{key}.json", 'w') do |f|
    f.write(JSON.pretty_generate({
      '$ref': "swagger.json#/definitions/#{key}"
    }))
  end
end

إليك كيف سيبدو الاختبار:

describe 'GET /users' do
  it 'List all users' do
    get '/users'

    expect_status(200)
    expect(result[:users][0]).to match_json_schema('User')
  end
end

كتابة الاختبارات بهذه الطريقة مريحة للغاية. خاصة إذا كان IDE الخاص بك يدعم إجراء الاختبارات وتصحيح الأخطاء (على سبيل المثال WebStorm و RubyMine و Visual Studio). بهذه الطريقة يمكنك تجنب استخدام برامج أخرى وتقتصر دورة تطوير واجهة برمجة التطبيقات بالكامل على ثلاث خطوات:

  • تصميم المواصفات في ملفات تينيسبيك.
  • كتابة مجموعة كاملة من الاختبارات لنقاط النهاية المضافة / المحررة.
  • تنفيذ الكود الذي يرضي الاختبارات.

2. التحقق من صحة بيانات الإدخال

لا يصف OpenAPI تنسيق الاستجابة فحسب بل يصف أيضًا بيانات الإدخال. يتيح لك ذلك التحقق من صحة البيانات التي يرسلها المستخدم في وقت التشغيل والتأكد من تحديثات قاعدة البيانات المتسقة والآمنة.

لنفترض أن لدينا المواصفات التالية التي تصف تصحيح سجل المستخدم وجميع الحقول المتاحة التي يُسمح بتحديثها:

# user.models.tinyspec
UserUpdate !{name?, age?: i}

# users.endpoints.tinyspec
PATCH /users/:id {user: UserUpdate}
    => {success: b}

في السابق استكشفنا المكونات الإضافية للتحقق من الصحة أثناء الاختبار ولكن بالنسبة للحالات العامة توجد وحدات التحقق من صحة ajv (npm) و json-schema (rubygem). دعونا نستخدمها لكتابة وحدة تحكم مع التحقق من الصحة:

Node.js (Koa)

هذا مثال لـ Koa خليفة Express – لكن كود Express المكافئ سيبدو مشابهًا.

import Router from 'koa-router';
import Ajv from 'ajv';
import { schemas } from './schemas';

const router = new Router();

// Standard resource update action in Koa.
router.patch('/:id', async (ctx) => {
  const updateData = ctx.body.user;

  // Validation using JSON schema from API specification.
  await validate(schemas.UserUpdate, updateData);

  const user = await User.findById(ctx.params.id);
  await user.update(updateData);

  ctx.body = { success: true };
});

async function validate(schema, data) {
  const ajv = new Ajv();

  if (!ajv.validate(schema, data)) {
    const err = new Error();
    err.errors = ajv.errors;
    throw err;
  }
}

في هذا المثال يعرض الخادم استجابة 500 خطأ خادم داخلي إذا كان الإدخال لا يتطابق مع المواصفات. لتجنب ذلك يمكننا اكتشاف خطأ المدقق وتشكيل إجابتنا الخاصة التي ستحتوي على معلومات أكثر تفصيلاً حول الحقول المحددة التي فشلت في التحقق من الصحة واتباع المواصفات.

دعونا نضيف تعريف FieldsValidationError:

# error.models.tinyspec
Error {error: b, message}

InvalidField {name, message}

FieldsValidationError < Error {fields: InvalidField[]}

والآن دعنا ندرجها كواحدة من ردود نقطة النهاية المحتملة:

# users.endpoints.tinyspec
PATCH /users/:id {user: UserUpdate}
    => 200 {success: b}
    => 422 FieldsValidationError

يتيح لك هذا الأسلوب كتابة اختبارات الوحدة التي تختبر صحة سيناريوهات الخطأ عندما تأتي البيانات غير الصالحة من العميل.

3. تسلسل النموذج (model serialization)

تستخدم جميع أطر عمل الخوادم الحديثة تقريبًا تعيين ارتباط الكائنات  (ORM) بطريقة أو بأخرى. هذا يعني أن غالبية الموارد التي تستخدمها API يتم تمثيلها بواسطة النماذج ومثيلاتها ومجموعاتها.

تسمى عملية تكوين تمثيلات JSON لهذه الكيانات التي سيتم إرسالها في الاستجابة بالتسلسل.

هناك عدد من المكونات الإضافية لإجراء التسلسل: على سبيل المثال Sequelize-to-json (npm) و act_as_api (rubygem) و jsonapi-rails (rubygem). بشكل أساسي تسمح لك هذه المكونات الإضافية بتوفير قائمة الحقول الخاصة بنموذج معين يجب تضمينه في كائن JSON بالإضافة إلى القواعد الإضافية. على سبيل المثال يمكنك إعادة تسمية الحقول وحساب قيمها ديناميكيًا.

يصبح الأمر أكثر صعوبة عندما تحتاج إلى عدة تمثيلات JSON مختلفة لنموذج واحد أو عندما يحتوي الكائن على كيانات متداخلة – ارتباطات. ثم تبدأ في الحاجة إلى ميزات مثل ربط الوراثة وإعادة الاستخدام والمسلسل.

توفر الوحدات النمطية المختلفة حلولًا مختلفة ولكن دعنا نفكر في هذا: هل يمكن أن تساعد المواصفات مرة أخرى؟ بشكل أساسي جميع المعلومات حول متطلبات تمثيلات JSON جميع مجموعات الحقول الممكنة بما في ذلك الكيانات المضمنة موجودة بالفعل فيها. وهذا يعني أنه يمكننا كتابة متسلسل آلي واحد.

اسمحوا لي أن أقدم الوحدة الصغيرة للتسلسل التسلسلي  (npm) والتي تدعم القيام بذلك لنماذج Sequelize. يقبل مثيل نموذج أو مصفوفة والمخطط المطلوب ثم يتكرر من خلاله لبناء الكائن المتسلسل. كما أنها تمثل جميع الحقول المطلوبة وتستخدم المخططات المتداخلة للكيانات المرتبطة بها.

لذلك لنفترض أننا بحاجة إلى إعادة جميع المستخدمين الذين لديهم مشاركات في المدونة بما في ذلك التعليقات على هذه المنشورات من واجهة برمجة التطبيقات. دعنا نصفها بالمواصفات التالية:

# models.tinyspec
Comment {authorId: i, message}
Post {topic, message, comments?: Comment[]}
User {name, isAdmin: b, age?: i}
UserWithPosts < User {posts: Post[]}

# blogUsers.endpoints.tinyspec
GET /blog/users
    => {users: UserWithPosts[]}

يمكننا الآن إنشاء الطلب باستخدام Sequelize وإرجاع الكائن المتسلسل الذي يتوافق مع المواصفات الموضحة أعلاه تمامًا:

import Router from 'koa-router';
import serialize from 'sequelize-serialize';
import { schemas } from './schemas';

const router = new Router();

router.get('/blog/users', async (ctx) => {
  const users = await User.findAll({
    include: [{
      association: User.posts,
      required: true,
      include: [Post.comments]
    }]
  });

  ctx.body = serialize(users, schemas.UserWithPosts);
});

هذا شبه سحري ، أليس كذلك؟

4. الكتابة الثابتة (static typing)

إذا كنت رائعًا بما يكفي لاستخدام TypeScript أو Flow فربما تكون قد سألت بالفعل “ماذا عن الأنواع الثابتة الثمينة الخاصة بي؟!” باستخدام الوحدات النمطية sw2dts أو swagger-to-flowtype يمكنك إنشاء جميع الأنواع الثابتة اللازمة بناءً على مخططات JSON واستخدامها في الاختبارات ووحدات التحكم والمسلسلات.

tinyspec -j

sw2dts ./swagger.json -o Api.d.ts --namespace Api

الآن يمكننا استخدام الأنواع في وحدات التحكم:

router.patch('/users/:id', async (ctx) => {
  // Specify type for request data object
  const userData: Api.UserUpdate = ctx.request.body.user;

  // Run spec validation
  await validate(schemas.UserUpdate, userData);

  // Query the database
  const user = await User.findById(ctx.params.id);
  await user.update(userData);

  // Return serialized result
  const serialized: Api.User = serialize(user, schemas.User);
  ctx.body = { user: serialized };
});

والاختبارات:

it('Update user', async () => {
  // Static check for test input data.
  const updateData: Api.UserUpdate = { name: MODIFIED };

  const res = await request.patch('/users/1', { user: updateData });

  // Type helper for request response:
  const user: Api.User = res.body.user;

  expect(user).to.be.validWithSchema(schemas.User);
  expect(user).to.containSubset(updateData);
});

لاحظ أنه يمكن استخدام تعريفات الأنواع التي تم إنشاؤها ليس فقط في مشروع API ولكن أيضًا في مشاريع تطبيق العميل لوصف الأنواع في الوظائف التي تعمل مع API.( سيكون المطورون الزاويون سعداء بشكل خاص بهذا الأمر).

5. تبدیل أنواع سلاسل الاستعلام

إذا كانت واجهة برمجة التطبيقات الخاصة بك تستهلك لسبب ما الطلبات مع نوع التطبيق / x-www-form-urlencoded MIME بدلاً من application / json سيبدو نص الطلب كما يلي:

param1=value&param2=777&param3=false

الشيء نفسه ينطبق على معلمات الاستعلام (على سبيل المثال في طلبات GET). في هذه الحالة سيفشل خادم الويب في التعرف تلقائيًا على الأنواع: ستكون جميع البيانات بتنسيق سلسلة لذلك بعد التحليل ستحصل على هذا الكائن:

{ param1: 'value', param2: '777', param3: 'false' }

في هذه الحالة سيفشل الطلب في التحقق من صحة المخطط لذلك تحتاج إلى التحقق من تنسيقات المعلمات الصحيحة يدويًا ونقلها إلى الأنواع الصحيحة.

كما يمكنك التخمين يمكنك القيام بذلك باستخدام مخططاتنا القديمة الجيدة من المواصفات. لنفترض أن لدينا نقطة النهاية والمخطط التالي:

# posts.endpoints.tinyspec
GET /posts?PostsQuery

# post.models.tinyspec
PostsQuery {
  search,
  limit: i,
  offset: i,
  filter: {
    isRead: b
  }
}

إليك كيف يبدو الطلب إلى نقطة النهاية هذه:

GET /posts?search=needle&offset=10&limit=1&filter[isRead]=true

دعونا نكتب دالة castQuery لتحويل جميع المعلمات إلى الأنواع المطلوبة:

function castQuery(query, schema) {
  _.mapValues(query, (value, key) => {
    const { type } = schema.properties[key] || {};
  
    if (!value || !type) {
      return value;
    }
  
    switch (type) {
      case 'integer':
        return parseInt(value, 10);
      case 'number':
        return parseFloat(value);
      case 'boolean':
        return value !== 'false';
      default:
        return value;
    }
 });
}

يتوفر تنفيذ أكمل مع دعم للمخططات المتداخلة والمصفوفات والأنواع الخالية في الوحدة النمطية cast-with-schema (npm). الآن دعنا نستخدمه في الكود الخاص بنا:

router.get('/posts', async (ctx) => {
  // Cast parameters to expected types
  const query = castQuery(ctx.query, schemas.PostsQuery);

  // Run spec validation
  await validate(schemas.PostsQuery, query);

  // Query the database
  const posts = await Post.search(query);

  // Return serialized result
  ctx.body = { posts: serialize(posts, schemas.Post) };
});

لاحظ أن ثلاثة من الأسطر الأربعة للكود تستخدم مخططات المواصفات.

المصدر

منشور ذات صلة
3d-printing 10 Minutes

مقدمة في الطباعة ثلاثية الأبعاد

جاسم ناظري

تستخدم الطباعة ثلاثية الأبعاد التصميم بمساعدة الكمبيوتر  (CAD) لإنشاء كائنات ثلاثية الأبعاد من خلال طريقة الطبقات. يشار إليها أحيانًا باسم التصنيع الإضافي تتضمن الطباعة ثلاثية الأبعاد مواد طبقات مثل البلاستيك أو المواد المركبة أو المواد الحيوية لإنشاء كائنات تتراوح في الشكل والحجم والصلابة واللون.

عنوان IP 6 Minutes

مقدمة عن عنونة IP المصنفة

جاسم ناظري

عنوان IP هو عنوان يحتوي على معلومات حول كيفية الوصول إلى مضيف معين، خاصة خارج الشبكة المحلية. عنوان IP هو عنوان فريد 32 بت يحتوي على مساحة عنوان 232.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

السلة