توليد الشفرة من OpenAPI v3 (المعروف أيضًا باسم Swagger 3) إلى TypeScript وليس فقط

قبل عامين ، بدأت التنمية أكثر واحد منشئ رمز مجاني من مواصفات OpenAPI v3 إلى TypeScript ( يتوفر على Github ). في البداية ، شرعت في إنشاء توليد فعال لأنواع البيانات البدائية والمعقدة في TypeScript ، مع مراعاة ميزات مخطط JSON المختلفة ، مثل oneOf / anyOf / allOf ، إلخ. ( حل Swagger الأصلي كان لديه بعض المشاكل في هذا). كانت هناك فكرة أخرى تتمثل في استخدام المخططات من المواصفات للتحقق من الصحة على الجزء الأمامي والخلفي وأجزاء أخرى من النظام.



الآن أصبح مولد الشفرات جاهزًا نسبيًا - إنه في مرحلة MVP . يحتوي على الكثير مما هو مطلوب من حيث إنشاء أنواع البيانات ، بالإضافة إلى مكتبة تجريبية لتوليد خدمات الواجهة الأمامية (حتى الآن للزاوية). في هذه المقالة ، أرغب في عرض التطورات وأقول كيف يمكنهم المساعدة إذا كنت تستخدم TypeScript و OpenAPI v3. على طول الطريق ، أود أن أشارك بعض الأفكار والاعتبارات التي نشأت في عملية عملي. حسنًا ، إذا كنت مهتمًا ، يمكنك قراءة القصة الخلفية التي اختبأتها في المفسد حتى لا تعقد قراءة الجزء الفني.


محتوى


  1. قبل التاريخ
  2. وصف
  3. التثبيت والاستخدام
  4. ممارسة باستخدام مولد رمز
  5. باستخدام أنواع البيانات التي تم إنشاؤها في التطبيقات
  6. تحلل الدوائر داخل مواصفات OAS
  7. التحلل المتداخل
  8. خدمات تم إنشاؤها تلقائيًا للعمل مع واجهة برمجة تطبيقات REST
    1. لماذا هذا مطلوب؟
    2. توليد الخدمات
    3. باستخدام الخدمات المولدة
  9. بدلا من الكلمة الأخيرة


قبل التاريخ


توسيع لقراءة (تخطي)

بدأ كل شيء منذ عامين - ثم عملت في شركة تقوم بتطوير منصة تعدين البيانات وكنت مسؤولاً عن الواجهة الأمامية (بشكل أساسي TypeScript + Angular). كانت ميزات المشروع هياكل بيانات معقدة تحتوي على عدد كبير من المعلمات (30 أو أكثر) وليس دائمًا علاقات عمل واضحة بينها. كانت الشركة تنمو ، وبيئة البرمجيات كانت تمر بتغيرات متكررة للغاية. يجب أن تكون الواجهة الأمامية على دراية بالفروق الدقيقة ، لأن بعض الحسابات قد تم تكرارها في المقدمة وفي الواجهة الخلفية. وهذا هو ، كان هذا هو الحال عند استخدام OpenAPI أكثر من المناسب. لقد وجدت فترة في الشركة عندما حصل فريق التطوير في غضون أشهر على مواصفات واحدة ، والتي أصبحت قاعدة معرفة مشتركة للجزء الأمامي والخلفي وحتى قسم Core ، والتي كانت مخبأة خلف الخلفية الواسعة للويب. تم اختيار إصدار OpenAPI "للنمو" - ثم لا يزال شابًا الإصدار 3.0


لم تعد هذه مواصفات في ملف YML / JSON ثابت واحد أو أكثر ، وليست ناتجة عن التعليقات التوضيحية ، ولكن مكتبة كاملة من المكونات والطرق والقوالب والخصائص ، تم تنظيمها وفقًا لمفهوم DDD الخاص بالمنصة. تم تقسيم المكتبة إلى أدلة وملفات ، وأنتج جامع مرتب خصيصًا مستندات OAS لكل مجال موضوع. تم إنشاء طريقة تجريبية لتدفق العمل ، والتي يمكن وصفها بالتصميم أولاً.


توجد مقالة جيدة في مدونة شركة Yandex.Money ، والتي تحدثت عن Design First

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


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


شعرت بخيبة أمل. كانت هناك مشكلتان: OAS الإصدار 3.0 ، وبدعم ، على ما يبدو ، لم يكن أحد في عجلة من أمرنا ، وجودة الحلول نفسها - في ذلك الوقت (أتذكر أنه كان قبل عامين) ، تمكنت من العثور على حلين جاهزين نسبيًا: من Swagger ومن Microsoft (يبدو ذلك ). في البداية ، كان دعم OAS 3.0 في مرحلة تجريبية عميقة. عملت الثانية فقط مع الإصدار 2.x ، ولكن لم تكن هناك توقعات لا لبس فيها. بالمناسبة ، لم أتمكن من بدء تشغيل مُنشئ رموز Microsoft حتى في مستند اختبار بتنسيق Swagger 2.0. نجح الحل من Swagger ، لكن مخططًا أكثر أو أقل تعقيدًا مع ارتباطات $ ref تحولت إلى "خطأ!" ، وأرسلت التبعيات العودية إلى حلقة لا نهائية. كانت هناك مشاكل مع الأنواع البدائية . بالإضافة إلى ذلك ، لم أكن أفهم تمامًا كيفية العمل مع الخدمات التي يتم إنشاؤها تلقائيًا - يبدو أنها مصممة للعرض ، كما أن استخدامها الفعلي خلق مشاكل أكثر مما حل (في رأيي). وأخيرًا ، كان دمج ملف JAR في CI / CD موجهًا إلى NPM غير مريح: اضطررت إلى تنزيل اللقطة الضرورية يدويًا ، والتي بدت وكأنها تزن 13 ميغابايت ، وقمت بشيء ما. بشكل عام ، أخذت استراحة وقررت مشاهدة ما سيحدث بعد ذلك.


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


في ما بين الحفلات والأعياد ، كنت بحاجة إلى القيام بشيء ما ، وقررت أن أكتب قراري. كان العمل في المقاهي الصيفية بالقرب من حديقة Vake Park مثمرًا بشكل مدهش ، وعدت إلى بيتر مع مولد رمز جاهز لأنواع البيانات. ثم لمدة شهر آخر "انتهيت" من الخدمات في عطلات نهاية الأسبوع قبل أن يكون جاهزًا للعمل.


منذ البداية ، جعلت مولد الشفرة مفتوحًا ، وأعمل عليه في وقت فراغي. رغم أنه ، في الواقع ، كتب لمسودة العمل. لن أقول إن المراجعة / التشغيل قد ذهبت دون أي مشاكل ؛ ولن أقول أنها كانت كبيرة. لكن في مرحلة ما لاحظت أنني توقفت عن استخدام وثائق Redoc / Swagger: كان التنقل بالكود أكثر ملاءمة ، بشرط أن يكون الكود دائمًا محدّثًا والتعليق عليه. بعد فترة وجيزة ، "سجلت" إنجازاتي ، دون تطويرها بأي شكل من الأشكال ، حتى نصحني أحد الزملاء (منذ ستة أشهر سابقًا قبل أن أغادر إلى شركة أخرى) بأخذها بجدية أكبر (لقد جاء أيضًا بالاسم).


لم يكن لدي وقت فراغ كافٍ ، واستغرق الأمر عدة أشهر حتى استكملته في الخلفية: ملعب ، تطبيق اختبار ، إعادة تنظيم المشروع. الآن أنا مستعد لتلقي الملاحظات.


وصف


في الوقت الحالي ، يشتمل حل إنشاء الشفرات على ثلاث مكتبات NPM مدمجة فيcodegena osprey وتقع في مستودع أحادي مشترك:


المكتبةوصف
@ codegena / oapi3tsالمكتبة الأساسية عبارة عن محول من OAS3 إلى أوصاف نوع البيانات (يدعم الآن TypeScript فقط)
@ codegena / ng-api-serviceامتداد للخدمات الزاوية
@ codegena / oapi3ts-cliشل للاستخدام مريحة في البرامج النصية CLI


التثبيت والاستخدام


الخيار الأكثر عملية هو استخدام في البرامج النصية NodeJS تشغيل من CLI. تحتاج أولاً إلى تثبيت التبعيات:


 npm i @codegena/oapi3ts, @codegena/ng-api-service, @codegena/oapi3ts-cli 

بعد ذلك ، قم بإنشاء ملف js (مثل update-typings.js ) مع الكود:


 "use strict"; var cliLib = require('@codegena/oapi3ts-cli'); var cliApp = new cliLib.CliApplication; cliApp.createTypings(); // cliApp.createServices('angular'); // optional 

وابدأها بتمرير ثلاثة معايير:


 node ./update-typings.js --srcPath ./specs/todo-app-spec.json --destPath ./src/lib --separatedFiles true 

في destPath سيتم إنشاء ملفات ، وفي الواقع ، يتم إنشاء محتويات هذا الدليل في مستودع المشروع بالطريقة نفسها. فيما يلي النص البرمجي الذي يتم إنشاؤه ، وهكذا يتم تشغيله في البرامج النصية NPM. ومع ذلك ، إذا كنت ترغب في ذلك ، يمكنك استخدامه حتى في المتصفح ، كما هو الحال في الملعب .



ممارسة باستخدام مولد رمز


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


حتى إذا لم تكن معتادًا على Angular و / أو NestJS ، فلا يجب أن يتسبب ذلك في حدوث مشكلات: يجب أن يفهم معظم مطوري TypeScript أمثلة التعليمات البرمجية التي سيتم توفيرها.

على الرغم من أن التطبيق يتم تبسيطه قدر الإمكان (على سبيل المثال ، تقوم الواجهة الخلفية بتخزين البيانات في جلسة ، وليس في قاعدة البيانات) ، لقد حاولت إعادة إنشاء تدفق البيانات وميزات التسلسل الهرمي لأنواع البيانات الملازمة للتطبيق الحقيقي. تبلغ حوالي 80-85٪ جاهزة ، ولكن يمكن تأخير "الانتهاء" ، لكن من المهم الآن التحدث عما هو موجود بالفعل.



باستخدام أنواع البيانات التي تم إنشاؤها في التطبيقات


افترض أن لدينا مواصفات OpenAPI (على سبيل المثال ، هذا واحد ) التي يتعين علينا التعامل معها. لا يهم إذا أنشأنا شيئًا ما من نقطة الصفر ، أو دعمنا ، فهناك شيء مهم من المرجح أن نبدأ به - الكتابة. سنبدأ إما في وصف أنواع البيانات الأساسية أو إجراء تغييرات عليها. يقوم معظم المبرمجين بذلك من أجل تسهيل تطورهم في المستقبل. لذلك لا يتعين عليك النظر في الوثائق مرة أخرى ، ضع في اعتبارك قوائم المعلمات ؛ ويمكنك التأكد من أن IDE و / أو المترجم ستلاحظ وجود خطأ مطبعي.


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


 @Controller('group') export class AppController { // ... @Put(':groupId') rewriteGroup( @Param(ParseQueryPipe) { groupId }: RewriteGroupParameters, @Body() body: RewriteGroupRequest, @Session() session ): RewriteGroupResponse<HttpStatus.OK> { return this.appService .setSession(session) .rewriteGroup(groupId, body); } // ... } 

هذا هو جزء تحكم لإطار عمل NestJS مع المعلمات ( RewriteGroupParameters ) ، RewriteGroupParameters الطلب ( RewriteGroupRequest ) RewriteGroupRequest الاستجابة ( RewriteGroupResponse<T> ) RewriteGroupResponse<T> . في جزء الشفرة هذا بالفعل ، يمكننا رؤية فوائد الكتابة:


  • إذا groupId بين اسم المعلمة التدميرية groupId ، مع تحديد groupId بدلاً من ذلك ، groupId على الفور على خطأ في المحرر.
  • في حالة كتابة أسلوب this.appService.rewriteGroup (groupId ، نص) للمعلمات ، يمكننا التحكم في صحة المعلمة النصية التي تم تمريرها. وإذا تغير تنسيق بيانات الإدخال لطريقة التحكم أو طريقة الخدمة ، فسنعلم على الفور بذلك. بالنظر إلى المستقبل ، لاحظ أن طريقة الإدخال الخاصة بأسلوب الخدمة تحتوي على نوع بيانات مختلف عن RewriteGroupRequest ، ولكن في حالتنا ، ستكون متطابقة مع بعضها البعض. ومع ذلك ، إذا تم تغيير طريقة الخدمة فجأة وبدأت في قبول ToDoGroup بدلاً من ToDoGroupBlank ، ToDoGroupBlank IDE والمترجم الفوري أماكن التباين:
  • بنفس الطريقة ، يمكننا التحكم في الامتثال للنتيجة التي تم إرجاعها. إذا تغيرت مواصفات حالة الاستجابة الناجحة وأصبحت 202 بدلاً من 200 ، RewriteGroupResponse عليها أيضًا ، لأن RewriteGroupResponse هو نوع عام بنوع معدود :

الآن ، دعونا نلقي نظرة على مثال من التطبيق الأمامي الذي يعمل مع طريقة API أخرى :


 protected initSelectedGroupData(truth: ComponentTruth): Observable<ComponentTruth> { return this.getGroupsService.request(null, { isComplete: null, withItems: false }).pipe( pickResponseBody<GetGroupsResponse<200>>(200, null, true), switchMap<ToDoGroup[], Observable<ComponentTruth>>( groups => this.loadItemsOfSelectedGroups({ ...truth, groups }) ) ); } 

دعنا لا نتقدم على أنفسنا ونحلل عامل التشغيل المخصص لـ RxJS pickResponseBody ، ولكن دعونا نركز على تحسين نوع GetGroupsResponse . نحن نستخدمها في سلسلة من مشغلي RxJS ، والمشغل الذي يتبعه لديه تحسين المدخلات من ToDoGroup[] . إذا كان هذا الرمز يعمل ، فإن أنواع البيانات المشار إليها تتوافق مع بعضها البعض. هنا يمكننا أيضًا التحكم في مطابقة الكتابة ، وإذا تغير تنسيق الاستجابة في واجهة برمجة التطبيقات لدينا فجأة ، فلن يفلت هذا من اهتمامنا:



وبالطبع ، this.getGroupsService.request كتابة معلمات الاتصال الخاصة بـ this.getGroupsService.request أيضًا. ولكن هذا هو موضوع الخدمات المولدة.


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


تحلل الدوائر داخل مواصفات OAS


من المحتمل ، في الأمثلة السابقة ، الانتباه إلى ToDoGroup RewriteGroupResponse و GetGroupsResponse و GetGroupsResponse . في الواقع ، RewriteGroupResponse هو مجرد اسم مستعار عام لـ ToDoGroup و HttpErrorBadRequest ، إلخ. من السهل تخمين أن كلا من ToDoGroup و HttpErrorBadRequest هما المخططات من قسم مواصفات المكونات. المشار إليها في نقطة نهاية rewriteGroup (مباشرة أو من خلال الوسطاء ):


 "responses": { "200": { "description": "Todo group saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ToDoGroup" } } } }, "400": { "$ref": "#/components/responses/errorBadRequest" }, "404": { "$ref": "#/components/responses/errorGroupNotFound" }, "409": { "$ref": "#/components/responses/errorConflict" }, "500": { "$ref": "#/components/responses/errorServer" } } 

هذا هو التحلل المعتاد لهياكل البيانات ، ومبدأه هو نفسه في لغات البرمجة الأخرى. المكونات ، بدورها ، يمكن أن تتحلل: الرجوع إلى المكونات الأخرى (بما في ذلك بشكل متكرر) ، واستخدام مزيج وغيرها من ميزات مخطط JSON. ولكن بغض النظر عن التعقيد ، يجب تحويلها بشكل صحيح إلى أوصاف لأنواع البيانات. أريد أن أوضح كيف يمكنك استخدام التحلل في OpenAPI ، وكيف سيبدو الرمز الذي تم إنشاؤه.


سوف تتداخل المكونات في مواصفات OAS جيدة التصميم مع طراز DDD للتطبيقات التي تستخدمه. ولكن حتى لو كانت المواصفات غير كاملة ، يمكنك الاعتماد عليها ، وبناء نموذج البيانات الخاص بك. سوف يمنحك هذا مزيدًا من التحكم في مراسلات أنواع البيانات الخاصة بك مع أنواع بيانات الأنظمة الفرعية المتكاملة.

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


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

اتضح أنه بناءً على سيناريو الاستخدام ، لدينا هيكلان للبيانات: المهمة التي أنشأها المستخدم للتو ، والمهمة التي تم تخزينها بالفعل في قاعدة البيانات. في الحالة الثانية ، لها UID فريد ، تاريخ الإنشاء ، التغيير ، إلخ ، ويجب تعيين هذه البيانات في الواجهة الخلفية. وصفت كيانين ( ToDoTaskBlank و ToDoTask ) بطريقة أن الأول هو مجموعة فرعية من الثانية:


 "components": { "ToDoTaskBlank": { "title": "Base part of data of item in todo's group", "description": "Data about group item needed for creation of it", "properties": { "groupUid": { "description": "An unique id of group that item belongs to", "$ref": "#/components/schemas/Uid" }, "title": { "description": "Short brief of task to be done", "type": "string", "minLength": 3, "maxLength": 64 }, "description": { "description": "Detailed description and context of the task. Allowed using of Common Markdown.", "type": ["string", "null"], "minLength": 10, "maxLength": 1024 }, "isDone": { "description": "Status of task: is done or not", "type": "boolean", "default": "false", "example": false }, "position": { "description": "Position of a task in group. Allows to track changing of state of a concrete item, including changing od position.", "type": "number", "min": 0, "max": 4096, "example": 0 }, "attachments": { "type": "array", "description": "Any material attached to the task: may be screenshots, photos, pdf- or doc- documents on something else", "items": { "$ref": "#/components/schemas/AttachmentMeta" }, "maxItems": 16, "example": [] } }, "required": [ "isDone", "title" ], "example": { "isDone": false, "title": "Book soccer field", "description": "The complainant agreed and recruited more members to play soccer." } }, "ToDoTask": { "title": "Item in todo's group", "description": "Describe data structure of an item in group of tasks", "allOf": [ { "$ref": "#/components/schemas/ToDoTaskBlank" }, { "type": "object", "properties": { "uid": { "description": "An unique id of task", "$ref": "#/components/schemas/Uid", "readOnly": true }, "dateCreated": { "description": "Date/time (ISO) when task was created", "type": "string", "format": "date-time", "readOnly": true, "example": "2019-11-17T11:20:51.555Z" }, "dateChanged": { "description": "Date/time (ISO) when task was changed last time", "type": "string", "format": "date-time", "readOnly": true, "example": "2019-11-17T11:20:51.555Z" } }, "required": [ "dateChanged", "dateCreated", "position", "uid" ] } ] } } 

في الخرج ، نحصل على واجهتي TypeScript ، وسيتم توارث الأول بواسطة الثانية :


 /** * ## Base part of data of item in todo's group * Data about group item needed for creation of it */ export interface ToDoTaskBlank { // ... imagine there are ToDoTaskBlank properties } /** * ## Item in todo's group * Describe data structure of an item in group of tasks */ export interface ToDoTask extends ToDoTaskBlank { /** * ## UID of element * An unique id of task */ readonly uid: string; /** * Date/time (ISO) when task was created */ readonly dateCreated: string; /** * Date/time (ISO) when task was changed last time */ readonly dateChanged: string; // ... imagine there are ToDoTaskBlank properties } 

الآن لدينا الأوصاف الأساسية لكيان المهام ، ونشير إليها في كود طلبنا كما تم في التطبيق التجريبي :


 import { ToDoTask, ToDoTaskBlank, } from '@our-npm-scope/our-generated-lib'; export interface ToDoTaskTeaser extends ToDoTask { isInvalid?: boolean; /** * Means this task just created, has temporary uid * and not saved yet. */ isJustCreated?: boolean; /** * Means this task is saving now. */ isPending?: boolean; /** * Previous uid of task temporary assigned until * it gets saved and gets new UID from backend. */ prevTempUid?: string; } 

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


 export function downgradeTeaserToTask( taskTeaser: ToDoTaskTeaser ): ToDoTask { const task = { ...taskTeaser }; if (!task.description || !task.description.trim()) { delete task.description; } else { task.description = task.description.trim(); } delete task.isJustCreated; delete task.isPending; delete task.prevTempUid; return task; } export function downgradeTeaserToTaskBlank( taskTeaser: ToDoTaskTeaser ): ToDoTaskBlank { const task = downgradeTeaserToTask(taskTeaser) as any; delete task.dateChanged; delete task.dateCreated; delete task.uid; return task; } 

شخص ما يفضل جعل نموذج البيانات أكثر تكاملاً واستخدام الفصول.
 export class ToDoTaskTeaser implements ToDoTask { // … imagine, definitions from ToDoTask are here constructor( task: ToDoTask, public isInvalid?: boolean, public isJustCreated?: boolean, public isPending?: boolean, public prevTempUid?: string ) { Object.assign(this, task); } downgradeTeaserToTask(): ToDoTask { const task = {...this}; if (!task.description || !task.description.trim()) { delete task.description; } else { task.description = task.description.trim(); } delete task.isJustCreated; delete task.isPending; delete task.prevTempUid; return task; } downgradeTeaserToTaskBlank(): ToDoTaskBlank { // … some code } } 

ولكن هذه مسألة أسلوب وملاءمة وكيفية تطوير بنية التطبيق. بشكل عام ، بغض النظر عن النهج ، يمكننا الاعتماد على نموذج البيانات الأساسي والحصول على مزيد من التحكم في مطابقة الكتابة. لذلك ، إذا أصبح ToDoTask ما ، فسوف نعرف عن جميع أجزاء الكود التي تتطلب التحديث:




التحلل المتداخل


حتى الآن لدينا واجهة ToDoTask ويمكننا الرجوع إليها. وبالمثل ، سنقوم بوصف ToDoTaskGroup و ToDoTaskGroupBlank ، وسيحتويان على خصائص الأنواع ToDoTask و ToDoTaskBlank ، على التوالي. لكننا الآن سنقوم بتقسيم "مجموعة المهام" إلى مكونين ، وليس ثلاثة ،: من أجل الوضوح ، سنصف دلتا في ToDoGroupExtendedData . لذلك أريد أن أوضح مقاربة يتم فيها إنشاء مكون واحد من المكونين الآخرين:


 "ToDoGroup": { "allOf": [ { "$ref": "#/components/schemas/ToDoGroupBlank" }, { "$ref": "#/components/schemas/ToDoGroupExtendedData" } ] } 

بعد بدء إنشاء الشفرة ، نحصل على بنية TypeScript مختلفة قليلاً:


 export type ToDoGroup = ToDoGroupBlank & // Data needed for group creation ToDoGroupExtendedData; // Extended data has to be obtained after first save 

نظرًا لأن ToDoGroup ليس له "نص" خاص به ، يفضل مولد الشفرة تحويله إلى اتحاد للواجهات. ومع ذلك ، إذا أضفت الجزء الثالث بمخططك الخاص (مجهول) ، فستكون النتيجة واجهة مع أسلاف (لكن من الأفضل عدم القيام بذلك). ودعنا نلاحظ أن خاصية items لواجهة ToDoGroupBlank كتابتها كصفيف لـ ToDoTaskBlank ، ويتم إعادة تعريفها في ToDoGroupBlank على ToDoTask . وبالتالي ، فإن مولد الشفرة قادر على نقل الفروق الدقيقة المعقدة في التحلل من مخطط JSON إلى TypeScipt.


 /* tslint:disable */ import { ToDoTaskBlank } from './to-do-task-blank'; /** * ## Base part of data of group * Data needed for group creation */ export interface ToDoGroupBlank { // ... items?: Array<ToDoTaskBlank>; // ... } 

 /* tslint:disable */ import { ToDoTask } from './to-do-task'; /** * ## Extended data of group * Extended data has to be obtained after first save */ export interface ToDoGroupExtendedData { // ... items: Array<ToDoTask>; } 

حسنًا ، بالطبع ، في ToDoTask / ToDoTaskBlank يمكننا أيضًا استخدام التحلل. ربما لاحظت أن خاصية attachments موصوفة على أنها صفيف من عناصر النوع AttachmentMeta . ويتم وصف هذا المكون على النحو التالي:


 "AttachmentMeta": { "description": "Common meta data model of any type of attachment", "oneOf": [ {"$ref": "#/components/schemas/AttachmentMetaImage"}, {"$ref": "#/components/schemas/AttachmentMetaDocument"}, {"$ref": "#/components/schemas/ExternalResource"} ] } 

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


 /** * Any material attached to the task: may be screenshots, photos, pdf- or doc- * documents on something else */ attachments?: Array< | AttachmentMetaImage // Meta data of image attached to task | AttachmentMetaDocument // Meta data of document attached to task | string // Link to any external resource >; 

في الوقت نفسه ، بالنسبة لمكونات AttachmentMetaDocument و AttachmentMetaDocument ، يتم وصف الواجهات غير المجهولة التي يتم استيرادها في الملفات التي تستخدمها:


 import { AttachmentMetaDocument } from './attachment-meta-document'; import { AttachmentMetaImage } from './attachment-meta-image'; 

ولكن حتى في AttachmentMetaImage ، يمكننا العثور على رابط لواجهة ImageOptions أخرى تم تقديمها ، والتي يتم استخدامها مرتين ، بما في ذلك داخل واجهة مجهولة (نتيجة للتحويل من خصائص إضافية ):


 /* tslint:disable */ import { ImageOptions } from './image-options'; /** * Meta data of image attached to task */ export interface AttachmentMetaImage { // ... /** * Possible thumbnails of uploaded image */ thumbs?: { [key: string]: { /** * Link to any external resource */ url?: string; imageOptions?: ImageOptions; }; }; // ... imageOptions: ImageOptions; } 

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



خدمات تم إنشاؤها تلقائيًا للعمل مع واجهة برمجة تطبيقات REST



لماذا هذا مطلوب؟


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


  • تعيينات URL والمعلمات
  • التحقق من صحة المعلمات ، الطلب والاستجابة
  • استخراج البيانات والتعامل مع حالات الطوارئ

من غير السار أن هذا من نواح كثيرة نموذجي ولا يحتوي على أي منطق فريد. لنفترض بعض الأمثلة - كمخطط عام ، يمكن بناء العمل مع واجهة برمجة التطبيقات:


مثال تخطيطي مبسط للعمل مع واجهة برمجة تطبيقات REST
 import _ from 'lodash'; import { Observable, fromFetch, throwError } from 'rxjs'; import { switchMap } from 'rxjs/operators'; // Definitions const URLS = { 'getTasksOfGroup': `${env.REST_API_BASE_URL}/tasks/\${groupId}`, // ... other urls ... }; const URL_TEMPLATES = _.mapValues(urls, url => _.template(url)); interface GetTaskConditions { isDone?: true | false; offset?: number; limit?: number; } interface ErrorReponse { error: boolean; message?: string; } // Helpers // I taken this snippet from StackOverflow only for example function encodeData(data) { return Object.keys(data).map(function(key) { return [key, data[key]].map(encodeURIComponent).join("="); }).join("&"); } // REST API functions // our REST API working function example function getTasksFromServer(groupUid: string, conditions: GetTaskConditions = {}): Observable<Response> { if (!groupUid) { return throwError(new Error('You should specify "groupUid"!')); } if (!_.isString(groupUid)) { return throwError(new Error('`groupUid` should be string!')); } if (_.isBoolean(conditions.isDone)) { // ... applying of conditions.isDone } else if (conditions.isDone !== undefined) { return throwError(new Error('`isDone` should be "true", "false" or should\'t be set!'!)); } if (offset) { // ... check of `offset` and applying or error throwing } if (limit) { // ... check of `limit` and applying or error throwing } const url = [ URL_TEMPLATES['getTasksOfGroup']({groupUid}), ...(conditions ? [encodeData(conditions)] : []) ]; return fromFetch(url); } // Using of REST API working functions function getRemainedTasks(groupUid: number): Observable<ToDoTask[] | ErrorReponse> { return getTasksFromServer(groupUid, {isDone: false}).pipe( switchMap(response => { if (response.ok) { // OK return data return response.json(); } else { // Server is returning a status requiring the client to try something else. return of({ error: true, message: `Error ${response.status}` }); } }), catchError(err => { // Network or other error, handle appropriately console.error(err); return of({ error: true, message: err.message }) }) ); } 

يمكنك استخدام تجريد عالي المستوى للعمل مع REST - حسب المكدس المستخدم ، يمكن أن يكون: Axios أو Angular HttpClient أو أي حل آخر مشابه. لكن على الأرجح ، سيتوافق رمزك مع هذا المثال. بكل تأكيد ، سيتضمن:


  • خدمات أو وظائف للوصول إلى نقاط نهاية محددة (وظيفة getTasksFromServer في مثالنا)
  • أجزاء من التعليمات البرمجية التي تعالج النتيجة (دالة getRemainedTasks )

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


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

بالرجوع إلى موضوع OpenAPI ، نلاحظ أنه في مواصفات OAS قد تكون هناك معلومات كافية ل:


  • -
  • URL

. , , / — 5, 10 200, . , , : , , , RxJS- pickResponseBody , , - ; tapResponse , side-effect (tap) HTTP-. , - . , , .


, — -, . , , , "" / API "-" "" . - , "" ( ), .

, REST API Angular. , , /. . , , . , , .. .




" " . Angular-, update-typings.js :


 "use strict"; var cliLib = require('@codegena/oapi3ts-cli'); var cliApp = new cliLib.CliApplication; cliApp.createTypings(); cliApp.createServices('angular'); 

, Angular- API . , - - , . , RewriteGroupService . ApiService , , , -:


-
 // Typings for this API method import { RewriteGroupParameters, RewriteGroupResponse, RewriteGroupRequest } from '../typings'; // Schemas import { schema as domainSchema } from './schema.b4c655ec1635af1be28bd6'; /** * Service for angular based on ApiAgent solution. * Provides assured request to API method with implicit * validation and common errors handling scheme. */ @Injectable() export class RewriteGroupService extends ApiService< RewriteGroupResponse, RewriteGroupRequest, RewriteGroupParameters > { protected get method(): 'PUT' { return 'PUT'; } /** * Path template, example: `/some/path/{id}`. */ protected get pathTemplate(): string { return '/group/{groupId}'; } /** * Parameters in a query. */ protected get queryParams(): string[] { return ['forceSave']; } // ... } 

, JSON Schema , . , , :


 import { schema as domainSchema } from './schema.b4c655ec1635af1be28bd6'; 

, schema.b4c655ec1635af1be28bd6.ts , , .



, Angular-.


Angular-

ApiModule :


 import { ApiModule, API_ERROR_HANDLER } from '@codegena/ng-api-service'; import { CreateGroupItemService, GetGroupsService, GetGroupItemsService, UpdateFewItemsService } from '@codegena/todo-app-scheme'; @NgModule({ imports: [ ApiModule, // ... ], providers: [ RewriteGroupService, { provide: API_ERROR_HANDLER, useClass: ApiErrorHandlerService }, // ... ], // ... }) export class TodoAppModule { } 

, [])( https://angular.io/guide/dependency-injection ):


 @Injectable() export class TodoTasksStore { constructor( protected createGroupItemService: CreateGroupItemService, protected getGroupsService: GetGroupsService, protected getGroupItemsService: GetGroupItemsService, protected updateFewItemsService: UpdateFewItemsService ) {} } 

— , request , :


 return this.getGroupsService.request(null, { isComplete: null, withItems: false }).pipe( pickResponseBody<GetGroupsResponse<200>>(200, null, true), switchMap<ToDoGroup[], Observable<ComponentTruth>>( groups => this.loadItemsOfSelectedGroups({ ...truth, groups }) ) ); 

request Observable<HttpResponse<R> | HttpEvent<R>> , , . , , . , , , . RxJS- pickResponseBody .


, , , . API, . . , :



. JSON Schema . , "" - . , Sentry Kibana , . . , , .


, . , :)


بدلا من الكلمة الأخيرة


, . -, " " — . , , , .


— , - / ( ). , — .


.

Source: https://habr.com/ru/post/ar483628/


All Articles