Pembuatan kode dari OpenAPI v3 (alias Swagger 3) ke TypeScript dan tidak hanya

Dua tahun lalu, saya memulai pengembangan satu lagi generator kode gratis dari Spesifikasi OpenAPI v3 ke TypeScript ( tersedia di Github ). Awalnya, saya mulai membuat generasi efisien tipe data primitif dan kompleks dalam TypeScript, dengan mempertimbangkan berbagai fitur Skema JSON , seperti oneOf / anyOf / allOf , dll. ( Solusi asli Swagger memiliki beberapa masalah dengan ini). Gagasan lain adalah menggunakan skema dari spesifikasi untuk validasi di bagian depan, belakang dan bagian lain dari sistem.



Sekarang generator kode relatif siap - ini pada tahap MVP . Ini memiliki banyak hal yang diperlukan dalam hal menghasilkan tipe data, serta perpustakaan eksperimental untuk menghasilkan layanan front-end (sejauh ini untuk Angular). Pada artikel ini saya ingin menunjukkan perkembangan dan memberi tahu bagaimana mereka dapat membantu jika Anda menggunakan TypeScript dan OpenAPI v3. Sepanjang jalan, saya ingin berbagi beberapa ide dan pertimbangan yang muncul dalam proses kerja saya. Nah, jika Anda tertarik, Anda bisa membaca backstory yang saya sembunyikan di spoiler agar tidak menyulitkan pembacaan bagian teknis.


Isi


  1. Latar belakang
  2. Deskripsi
  3. Instalasi dan penggunaan
  4. Berlatih menggunakan generator kode
  5. Menggunakan tipe data yang dihasilkan dalam aplikasi
  6. Dekomposisi sirkuit dalam spesifikasi OAS
  7. Dekomposisi bersarang
  8. Layanan yang dibuat secara otomatis untuk bekerja dengan REST API
    1. Mengapa ini dibutuhkan?
    2. Generasi layanan
    3. Menggunakan layanan yang dihasilkan
  9. Alih-alih kata penutup


Latar belakang


Perluas untuk membaca (lewati)

Semuanya dimulai dua tahun yang lalu - kemudian saya bekerja untuk sebuah perusahaan yang mengembangkan platform Data Mining dan bertanggung jawab untuk frontend (terutama TypeScript + Angular). Fitur proyek adalah struktur data yang kompleks dengan sejumlah besar parameter (30 atau lebih) dan tidak selalu hubungan bisnis yang jelas di antara mereka. Perusahaan tumbuh, dan lingkungan perangkat lunak mengalami perubahan yang cukup sering. Frontend harus berpengetahuan luas dalam nuansa, karena beberapa perhitungan digandakan di depan dan di backend. Artinya, ini adalah kasus ketika menggunakan OpenAPI lebih dari tepat. Saya menemukan periode di perusahaan ketika dalam hitungan bulan tim pengembangan memperoleh spesifikasi tunggal, yang menjadi basis pengetahuan umum untuk bagian belakang, depan dan bahkan Core, yang tersembunyi di belakang bagian belakang backend web. Versi OpenAPI dipilih "untuk pertumbuhan" - maka v3.0 masih cukup muda


Ini bukan lagi spesifikasi dalam satu atau lebih file YML / JSON statis, dan bukan hasil annotator , tetapi seluruh pustaka komponen, metode, template, dan properti, yang diorganisasikan sesuai dengan konsep DDD platform. Perpustakaan dibagi menjadi direktori dan file, dan seorang kolektor yang diatur secara khusus menghasilkan dokumen OAS untuk setiap bidang studi. Cara eksperimental dibangun alur kerja, yang dapat digambarkan sebagai Desain-Pertama.


Ada artikel bagus di blog perusahaan Yandex.Money, yang membahas tentang Desain Pertama

Desain Pertama dan spesifikasi umum membantu mengurangi pengetahuan, tetapi masalah baru menjadi jelas - menjaga relevansi kode. Spesifikasi tersebut menjelaskan beberapa lusin metode dan lusinan (dan kemudian ratusan) entitas. Tetapi kode harus ditulis secara manual: tipe data, layanan untuk bekerja dengan REST, dll. Satu atau dua sprint dengan cerita paralel sangat mengubah gambar; menambah kompleksitas pada penggabungan beberapa cerita dan faktor manusia. Rutinitas terancam signifikan, dan solusinya tampak jelas - Anda memerlukan pembuatan kode. Lagi pula, spesifikasi OAS sudah berisi semua yang diperlukan agar tidak mengetik ulang secara manual. Tapi itu tidak sesederhana itu.


Frontend berada di akhir siklus produksi, jadi saya merasakan perubahan yang lebih menyakitkan daripada rekan-rekan dari departemen lain. Ketika merancang API REST, lingkungan backend memutuskan, dan bahkan setelah persetujuan "Design First", kelembaman tetap; untuk ujung depan, semuanya tampak kurang jelas. Bahkan, saya memahami ini sejak awal, dan mulai menyelidiki tanah di muka - ketika pembicaraan tentang spesifikasi "universal" baru saja dimulai. Tidak ada pembicaraan untuk menulis pembuat kode Anda sendiri; Saya hanya ingin menemukan sesuatu yang siap.


Saya kecewa. Ada dua masalah: OAS versi 3.0, dengan dukungan yang, sepertinya, tidak ada yang terburu-buru, dan kualitas solusi sendiri - pada waktu itu (saya ingat itu dua tahun lalu), saya berhasil menemukan dua solusi yang relatif siap pakai: dari Swagger dan dari Microsoft (sepertinya itu ). Pada yang pertama, dukungan untuk OAS 3.0 dalam versi beta yang mendalam. Yang kedua hanya bekerja dengan versi 2.x, tetapi tidak ada perkiraan yang jelas. Ngomong-ngomong, saya tidak dapat memulai generator kode Microsoft bahkan pada dokumen uji format Swagger 2.0. Solusi dari Swagger berhasil, tetapi skema yang kurang lebih rumit dengan tautan $ ref berubah menjadi “KESALAHAN!”, Dan dependensi rekursif mengirimnya ke loop tanpa batas. Ada masalah dengan tipe primitif . Selain itu, saya tidak begitu mengerti bagaimana bekerja dengan layanan yang dibuat secara otomatis - mereka tampaknya dibuat untuk pertunjukan, dan penggunaan mereka yang sebenarnya menciptakan lebih banyak masalah daripada yang mereka selesaikan (menurut saya). Dan akhirnya, integrasi file JAR ke CI / CD yang berorientasi NPM tidak nyaman: Saya harus mengunduh snapshot yang diperlukan secara manual, yang sepertinya berbobot 13 megabyte, dan melakukan sesuatu dengan itu. Secara umum, saya beristirahat dan memutuskan untuk menonton apa yang terjadi selanjutnya.


Setelah sekitar lima bulan, masalah pembuatan kode muncul kembali. Saya harus menulis ulang dan memperluas bagian dari aplikasi Web, dan pada saat yang sama saya ingin memperbaiki layanan lama untuk bekerja dengan REST API dan tipe data. Tetapi penilaian kompleksitas tidak optimis: dari satu minggu ke dua - dan ini hanya untuk layanan REST dan deskripsi tipe. Saya tidak akan mengatakan bahwa itu sangat membuat saya tertekan, tapi tetap saja. Di sisi lain, saya tidak pernah menemukan solusi untuk pembuatan kode dan tidak menunggu, dan implementasinya tidak akan memakan waktu lebih sedikit. Artinya, tidak ada pertanyaan tentang hal itu: manfaatnya diragukan, risikonya besar. Tidak ada yang akan mendukung gagasan ini, dan saya tidak mengusulkan. Sementara itu, liburan Mei sudah dekat, dan perusahaan "berhutang" kepada saya beberapa hari karena bekerja di akhir pekan. Selama dua minggu saya melarikan diri dari semua pengalaman kerja ke Georgia, tempat saya pernah tinggal selama hampir setahun.


Di sela-sela pesta dan pesta, saya perlu melakukan sesuatu, dan saya memutuskan untuk menulis keputusan saya. Bekerja di kafe musim panas di dekat Vake Park ternyata sangat produktif, dan saya kembali ke Peter dengan pembuat kode yang sudah jadi untuk tipe data. Kemudian untuk satu bulan lagi saya “menyelesaikan” layanan di akhir pekan sebelum dia siap bekerja.


Sejak awal, saya membuat generator kode terbuka, mengerjakannya di waktu luang saya. Meskipun, pada kenyataannya, ia menulis untuk konsep kerja. Saya tidak akan mengatakan bahwa revisi / run-in berjalan tanpa masalah; dan saya tidak akan mengatakan bahwa itu penting. Tetapi pada titik tertentu saya perhatikan bahwa saya berhenti menggunakan dokumentasi Redoc / Swagger: menavigasi kode lebih nyaman, asalkan kode selalu up-to-date dan dikomentari. Segera, saya "mencetak" pencapaian saya, tanpa mengembangkannya sama sekali, sampai seorang kolega (sekarang enam bulan lalu saya pergi ke perusahaan lain) menyarankan saya untuk menganggapnya lebih serius (dia juga datang dengan nama itu).


Saya tidak punya cukup waktu luang, dan beberapa bulan saya harus menyelesaikannya di latar belakang: taman bermain , aplikasi ujian, reorganisasi proyek. Sekarang saya siap menerima umpan balik.


Deskripsi


Saat ini, solusi untuk pembuatan kode mencakup tiga pustaka NPM yang terintegrasi dalam ocrey @codegena dan terletak di repositori mono umum:


PerpustakaanDeskripsi
@ codegena / oapi3tsPustaka dasar adalah konverter dari OAS3 ke deskripsi tipe data (sekarang hanya mendukung TypeScript)
@ codegena / ng-api-servicePerpanjangan untuk Layanan Angular
@ codegena / oapi3ts-cliShell agar mudah digunakan dalam skrip CLI


Instalasi dan penggunaan


Opsi paling praktis adalah menggunakan skrip NodeJS yang dijalankan dari CLI. Pertama, Anda perlu menginstal dependensi:


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

Lalu, buat file js (mis. update-typings.js ) dengan kode:


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

Dan mulai dengan melewati tiga parameter:


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

Di destPath akan ada file yang dihasilkan dan, pada kenyataannya, isi direktori ini di repositori proyek dibuat dengan cara yang sama. Ini adalah skrip penghasil , dan ini adalah bagaimana skrip NPM berjalan. Namun, jika mau, Anda dapat menggunakannya bahkan di browser, seperti yang dilakukan di Playground .



Berlatih menggunakan generator kode


Selanjutnya, saya ingin berbicara tentang apa yang akan kita dapatkan sebagai hasilnya: apa gagasan tentang bagaimana ini akan membantu kita. Bantuan visual akan menjadi kode aplikasi demo. Ini terdiri dari dua bagian: backend (pada kerangka NestJS ) dan frontend (pada Angular ). Jika mau, Anda bahkan dapat menjalankannya secara lokal .


Bahkan jika Anda tidak terbiasa dengan Angular dan / atau NestJS, ini seharusnya tidak menimbulkan masalah: contoh kode yang akan diberikan harus dipahami oleh sebagian besar pengembang TypeScript.

Meskipun aplikasi disederhanakan sebanyak mungkin (misalnya, backend menyimpan data dalam suatu sesi, bukan dalam database), saya mencoba untuk membuat ulang aliran data dan fitur-fitur dari hierarki tipe data yang melekat pada aplikasi sebenarnya. Sudah sekitar 80-85% siap, tetapi "selesai" bisa ditunda, tetapi untuk saat ini lebih penting untuk berbicara tentang apa yang sudah ada.



Menggunakan tipe data yang dihasilkan dalam aplikasi


Misalkan kita memiliki spesifikasi OpenAPI (misalnya, yang ini ) yang harus kita kerjakan. Tidak masalah jika kita membuat sesuatu dari awal, atau kita mendukung, ada hal penting yang kemungkinan besar akan kita mulai dengan - mengetik. Kami akan mulai menggambarkan tipe data dasar atau membuat perubahan padanya. Sebagian besar programmer melakukan ini untuk memfasilitasi pengembangan masa depan mereka. Jadi Anda tidak perlu melihat dokumentasi sekali lagi, perhatikan daftar parameter; dan Anda dapat yakin bahwa IDE dan / atau kompiler akan melihat kesalahan ketik.


Spesifikasi kami mungkin atau mungkin tidak termasuk bagian components.schems . Tetapi bagaimanapun, itu akan menggambarkan set parameter, permintaan dan jawaban - dan kita bisa menggunakannya. Pertimbangkan sebuah contoh:


 @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); } // ... } 

Ini adalah fragmen pengontrol untuk kerangka NestJS dengan parameter ( RewriteGroupParameters ), badan permintaan ( RewriteGroupRequest ) dan badan respons ( RewriteGroupResponse<T> ) yang RewriteGroupResponse<T> . Sudah dalam fragmen kode ini kita bisa melihat manfaat dari mengetik:


  • Jika kita mengacaukan nama groupId parameter yang dirusak, menentukan groupId sebagai gantinya, kami segera mendapatkan kesalahan di editor.
  • Jika metode this.appService.rewriteGroup (groupId, body) telah mengetik parameter, kita dapat mengontrol kebenaran dari parameter body dikirimkan. Dan jika format data input dari metode controller atau metode layanan berubah, kita akan segera mengetahuinya. RewriteGroupRequest depan, saya perhatikan bahwa metode input dari metode layanan memiliki tipe data yang berbeda dari RewriteGroupRequest , tetapi dalam kasus kami, mereka akan identik satu sama lain. Namun, jika tiba-tiba metode layanan diubah, dan mulai menerima ToDoGroup alih-alih ToDoGroupBlank , IDE dan kompiler akan segera menampilkan tempat-tempat perbedaan:
  • Dengan cara yang sama, kita dapat mengontrol kepatuhan terhadap hasil yang dikembalikan. Jika status respons yang sukses tiba-tiba berubah dalam spesifikasi dan menjadi 202 alih-alih 200 , kami juga akan mengetahuinya, karena RewriteGroupResponse adalah generik dengan tipe yang disebutkan :

Sekarang mari kita lihat contoh dari aplikasi front-end yang bekerja dengan metode API lain :


 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 }) ) ); } 

Mari kita tidak maju sendiri dan mem-parsing operator kustom RxJS pickResponseBody , tetapi mari kita fokus pada penyempurnaan tipe GetGroupsResponse . Kami menggunakannya dalam rantai operator RxJS, dan operator yang mengikutinya memiliki penyempurnaan input ToDoGroup[] . Jika kode ini berfungsi, maka tipe data yang ditunjukkan sesuai satu sama lain. Di sini kami juga dapat mengontrol pencocokan jenis, dan jika format respons di API kami tiba-tiba berubah, ini tidak akan luput dari perhatian kami:



Dan tentu saja, parameter panggilan this.getGroupsService.request juga diketik. Tapi ini adalah topik dari layanan yang dihasilkan.


Dalam contoh di atas, kita melihat bahwa mengetik permintaan, tanggapan, dan parameter dapat digunakan di berbagai bagian sistem - frontend, backend, dll. Jika backend dan frontend berada dalam repositori mono yang sama dan memiliki lingkungan yang kompatibel, mereka dapat menggunakan pustaka bersama yang sama dengan kode yang dihasilkan. Tetapi bahkan jika backend dan frontend didukung oleh tim yang berbeda, dan tidak memiliki kesamaan kecuali spesifikasi OAS publik, masih akan lebih mudah bagi mereka untuk menyinkronkan kode mereka.


Dekomposisi sirkuit dalam spesifikasi OAS


Mungkin, dalam contoh sebelumnya, Anda memperhatikan ToDoGroup , ToDoGroup , yang RewriteGroupResponse dengan RewriteGroupResponse dan GetGroupsResponse . Sebenarnya, RewriteGroupResponse hanyalah alias umum untuk ToDoGroup , HttpErrorBadRequest , dll. Sangat mudah untuk menebak bahwa ToDoGroup dan HttpErrorBadRequest adalah skema dari components.schem spesifikasi yang dirujuk oleh titik akhir rewriteGroup (langsung atau melalui perantara ):


 "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" } } 

Ini adalah dekomposisi struktur data yang biasa, dan prinsipnya sama dengan bahasa pemrograman lain. Komponen, pada gilirannya, juga dapat didekomposisi: lihat komponen lain (termasuk secara rekursif), gunakan kombinasi dan fitur JSON Schema lainnya. Tetapi terlepas dari kerumitannya, mereka harus dikonversi dengan benar ke deskripsi tipe data. Saya ingin menunjukkan bagaimana Anda dapat menggunakan dekomposisi di OpenAPI, dan bagaimana kode yang dihasilkan akan terlihat.


Komponen dalam spesifikasi OAS yang dirancang dengan baik akan tumpang tindih dengan model DDD dari aplikasi yang menggunakannya. Tetapi bahkan jika spesifikasinya tidak sempurna, Anda dapat mengandalkannya, membangun model data Anda sendiri. Ini akan memberi Anda kontrol lebih besar atas korespondensi tipe data Anda dengan tipe data subsistem terintegrasi.

Karena aplikasi kita adalah daftar tugas, esensi utama adalah Tugas. Adalah logis untuk memasukkannya ke dalam komponen di tempat pertama, karena entitas dan titik akhir lain entah bagaimana akan terhubung dengannya. Tetapi sebelum itu Anda perlu memahami dua hal:


  • Kami menjelaskan tidak hanya abstraksi, tetapi juga aturan validasi, dan semakin akurat dan jelas, semakin baik.
  • Seperti entitas apa pun yang disimpan dalam database, tugas memiliki dua jenis properti: layanan dan yang dimasukkan pengguna.

Ternyata, tergantung pada skenario penggunaan, kami memiliki dua struktur data: Tugas yang baru saja dibuat pengguna, dan Tugas yang sudah disimpan dalam database. Dalam kasus kedua, ia memiliki UID unik, tanggal pembuatan, perubahan, dll., Dan data ini harus ditetapkan pada backend. Saya menggambarkan dua entitas ( ToDoTaskBlank dan ToDoTask ) sedemikian rupa sehingga yang pertama adalah subset dari yang kedua:


 "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" ] } ] } } 

Pada output, kita mendapatkan dua antarmuka TypeScript, dan yang pertama akan diwarisi oleh yang kedua :


 /** * ## 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 } 

Sekarang kami memiliki deskripsi dasar entitas Task, dan kami merujuknya dalam kode aplikasi kami seperti yang dilakukan dalam aplikasi demo :


 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; } 

Dalam contoh ini, kami menggambarkan entitas baru, menambah ToDoTask properti-properti yang kami miliki di sisi aplikasi front-end. Faktanya, kami memperluas model data yang dihasilkan dengan mempertimbangkan kekhasan lokal. Di sekitar model ini, seperangkat alat lokal dan sesuatu seperti DTO primitif secara bertahap tumbuh:


 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; } 

Seseorang lebih suka membuat model data lebih integral dan menggunakan kelas.
 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 } } 

Tapi ini masalah gaya, kepantasan dan bagaimana arsitektur aplikasi akan berkembang. Secara umum, terlepas dari pendekatannya, kita dapat mengandalkan model data dasar dan memiliki kontrol lebih besar atas kesesuaian pengetikan. Jadi, jika karena alasan tertentu, uid dari ToDoTask menjadi angka, kita akan tahu tentang semua bagian dari kode yang perlu diperbarui:




Dekomposisi bersarang


Jadi sekarang kita memiliki antarmuka ToDoTask dan kita bisa ToDoTask . Demikian pula, kami akan menjelaskan ToDoTaskGroup dan ToDoTaskGroupBlank , dan mereka masing-masing berisi properti tipe ToDoTask dan ToDoTaskBlank . Tapi sekarang kita akan memecah "Kelompok Tugas" menjadi dua, bukan tiga, komponen: untuk kejelasan, kita akan menggambarkan delta di ToDoGroupExtendedData . Jadi saya ingin menunjukkan pendekatan di mana satu komponen dibuat dari dua komponen lainnya:


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

Setelah memulai pembuatan kode, kami mendapatkan konstruksi TypeScript yang sedikit berbeda:


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

Karena ToDoGroup tidak memiliki "tubuh" sendiri, pembuat kode lebih suka mengubahnya menjadi gabungan antarmuka. Namun, jika Anda menambahkan bagian ketiga dengan skema Anda sendiri (anonim), hasilnya akan menjadi antarmuka dengan dua leluhur (tetapi lebih baik tidak melakukannya). Dan mari kita perhatikan bahwa properti items dari antarmuka ToDoGroupBlank diketik sebagai array dari ToDoTaskBlank , dan didefinisikan ulang di ToDoGroupBlank di ToDoTask . Dengan demikian, pembuat kode dapat mentransfer nuansa dekomposisi yang agak rumit dari Skema JSON ke 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>; } 

Baik dan tentu saja, di ToDoTask / ToDoTaskBlank kita juga dapat menggunakan dekomposisi. Anda mungkin telah memperhatikan bahwa properti attachments dijelaskan sebagai array elemen bertipe AttachmentMeta . Dan komponen ini dijelaskan sebagai berikut:


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

Artinya, komponen ini mengacu pada komponen lain. Karena tidak memiliki skema sendiri, pembuat kode tidak membuatnya menjadi tipe data terpisah agar tidak menggandakan entitas, tetapi mengubah deskripsi anonim dari tipe yang disebutkan:


 /** * 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 >; 

Pada saat yang sama, untuk komponen AttachmentMetaDocument dan AttachmentMetaDocument , antarmuka non-anonim dijelaskan yang diimpor dalam file yang menggunakannya:


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

Tetapi bahkan di AttachmentMetaImage, kita dapat menemukan tautan ke antarmuka ImageOptions yang dirender lain, yang digunakan dua kali, termasuk di dalam antarmuka anonim (hasil konversi dari properti tambahan ):


 /* 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; } 

Dengan demikian, berdasarkan pada ToDoGroup atau ToDoGroup , kami benar-benar mengintegrasikan beberapa entitas ke dalam kode dan rangkaian koneksi bisnis mereka, yang memberi kami kontrol lebih besar atas perubahan dalam sistem berlebih yang melampaui kode kami. Tentu saja, ini tidak masuk akal dalam semua kasus. Tetapi jika Anda menggunakan OpenAPI, maka Anda mungkin memiliki satu bonus kecil lagi, di samping dokumentasi yang sebenarnya.



Layanan yang dibuat secara otomatis untuk bekerja dengan REST API



Mengapa ini dibutuhkan?


Jika kami mengambil aplikasi front-end statistik rata-rata yang berfungsi dengan REST API yang kurang lebih kompleks, maka sebagian besar kodenya adalah layanan (atau hanya fungsi) untuk mengakses API. Mereka akan mencakup:


  • Pemetaan URL dan Parameter
  • Validasi parameter, permintaan, dan respons
  • Ekstraksi Data dan Penanganan Darurat

Tidak menyenangkan bahwa dalam banyak hal ini tipikal dan tidak mengandung logika unik. Mari kita anggap beberapa contoh - sebagai garis besar umum, pekerjaan dengan API dapat dibangun:


Contoh skematis yang disederhanakan untuk bekerja dengan REST API
 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 }) }) ); } 

Anda dapat menggunakan abstraksi tingkat tinggi untuk bekerja dengan REST - tergantung pada tumpukan yang digunakan, dapat berupa: Axios , Angular HttpClient , atau solusi serupa lainnya. Tetapi kemungkinan besar, pada dasarnya kode Anda akan bertepatan dengan contoh ini. Hampir pasti, itu akan mencakup:


  • Layanan atau fungsi untuk mengakses titik akhir spesifik (fungsi getTasksFromServer dalam contoh kami)
  • Potongan kode yang memproses hasilnya (fungsi getRemainedTasks )

Dalam aplikasi dari dunia nyata, kode ini akan lebih rumit: spesifikasi aplikasi demo menjelaskan 5-6 opsi jawaban . Seringkali, REST API dirancang sedemikian rupa sehingga setiap status respons dari server harus ditangani sesuai. Tetapi bahkan memeriksa data input cenderung menjadi lebih sulit selama pengembangan aplikasi: semakin banyak waktu yang dibutuhkan untuk mendukung dan memproses ulasan kesalahan, semakin Anda ingin tahu tentang kemacetan dalam sirkulasi data dalam aplikasi.


Kesalahan dapat terjadi pada setiap titik dok dari bagian perangkat lunak, pendeteksian yang tidak tepat waktu (serta pencarian masalah yang sulit didiagnosis) bisa sangat mahal untuk bisnis. Karena itu, akan ada pemeriksaan klarifikasi tambahan. Sebagai basis kode tumbuh, dan jumlah kasus tertutup, demikian juga kompleksitas membuat perubahan. Tetapi bisnis adalah perubahan yang konstan, dan tidak ada jalan keluarnya. Karena itu, kita harus peduli tentang bagaimana kita akan membuat perubahan sebelumnya.

Kembali ke topik OpenAPI, kami mencatat bahwa dalam spesifikasi OAS mungkin ada informasi yang cukup untuk:


  • -
  • 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 , . . , , .


, . , :)


Alih-alih kata penutup


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


— , - / ( ). , — .


.

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


All Articles