ساخت API وبلاگ با فریمورک Swift Vapor 3 (قسمت اول)

ساخت API وبلاگ با فریمورک Swift Vapor 3 از پست قبلی که نوشتن برنامه Hello World با Vapor بود مدت زیادی گذشت. راستش قرار نبود اینقدر فاصله پست‌ها زیاد باشن اما دلیلش این بود که، منتظر بودم تا ورژن ۴ فریمورک Vapor منتشر بشه و مطالب آموزشی رو با اون بنویسم، چون خیلی خیلی کاملتر و در یک قسمت‌هایی استفاده ازش آسان‌تر هم هست. ولی ظاهرا طبق گفته یکی از دست‌اندرکاران پروژه تا چند ماه دیگه خبری از انتشار ورژن ۴ نیست. پس فعلا با Vapor 3 در خدمت‌تون هستم :)برای درک بهتر مطالب این سری نوشته ها بهتر است آشنایی کافی با زبان Swift بخصوص Generics و Closure و همچنین آشنایی اولیه با RESTful API داشته باشین.یک مفهوم جدید (EventLoopFuture یا Future)از اونجایی که Vapor یک فریمورک Async و Non-blocking هست (برای آشنایی مختصر با Non-blocking و Async به این نوشته من مراجعه کنید) هیچ دستوری باعث توقف مراحل اجرای برنامه نمیشه و به سرعت از روی اون دستور عبور کرده و دستور بعدی اجرا میشه. اما اگر واقعا زمان اجرای تابعی طولانی بود (مثلا خواندن از روی دیتابیس) چه اتفاقی میافته؟ در این صورت چنین تابعی نمیتونه بلافاصله جواب نهایی رو که به فرض یک array از رکوردهای دیتابیس هست رو برگردونه، از طرفی برنامه هم صبر نمیکنه تا اطلاعات از طرف دیتابیس خوانده و فرستاده بشن. پس خروجی تابع در آینده یک array خواهد بود و فعلا یک قول (Promise) داده میشه که بعدا (پس از خوانده شدن موفق اطلاعات از دیتابیس) یک array تحویل داده بشه. به این نوع خروجی EventLoopFuture یا در Vapor به اختصار Future گفته میشه (در واقع Future یک type alias است که Vapor به جای EventLoopFuture استفاده میکند و هیچ فرقی ندارد).اگر فرض کنیم تابع getPostsFromDatabase قرار باشه لیست پست‌های وبلاگ رو به صورت [Post] برگردونه، بعد از صدا زدن این تابع مقدار متغیر posts از نوع EventLoopFuture خواهد بود نه خود [Post]let posts = getPostsFromDatabase() // posts : EventLoopFuture<[Post]> مقدار این متغییر در واقع یک array نیست و هیچ کدام از دستوراتی که روی array ها اجرا میشوند، بر روی این متغییر اجرا نخواهند شد. خوب حالا چطوری میتونیم این مغییر رو به array تبدیل کنیم و مثلا اولین المان اون رو بگیریم و در کنسول پرینتش کنیم یا روی تمامی اعضای اون array لوپ بزنیم؟دستورات map و flatMapمیشه Future رو مانند یک متغیر optional در نظر بگیریم که قبل از استفاده از اون باید حتما unwrap بشه. در حال حاضر تنها راه unwrap کردن یک EventLoopFuture استفاده از این دو دستور بالا است. (تا وقتی که امکان async/await به زبان Swift اضافه بشه، که احتمالا در ورژن 6 خواهد بود ?). به عنوان مثال در تابع قبلی به این صورت میتونیم توی همه Post ها لوپ بزنیم و title هر پست رو چاپ کنیم. https://gist.github.com/hsharghi/b88377c04bf9742d95fcf087c7be8410 اگر داخل map قرار بود دوباره تابعی صدا زده بشه که باز هم جواب آن EventLoopFuture هست، مثلا میخواهیم برای هر post از دیتابیس کامنت های مربوط به اون پست خوانده بشه (این مثال صرفا برای نمایش استفاده از flatMap هست و به این صورت خواندن از دیتابیس کاملا اشتباه است) و به هر پست اضافه بشه، به جای map از flatMap استفاده میکنیم که بعدا با مثال توضیح داده میشه. https://gist.github.com/hsharghi/516344105ce070dccb56966605557c58 بله! مطلب در ابتدا تا حدودی به نظر پیچیده میاد. اما با چند بار استفاده کاملا به موضوع احاطه پیدا خواهید کرد و به راحتی از این دستورات استفاده میکنین. بذارین کمی از این دستورات در کدها استفاده کنیم تا بهتر متوجه بشیم.شروعساخت پروژهبرنامه Terminal رو باز کرده و دستور زیر را وارد کنید تا پروژه ساخته شده و در Xcode باز شود.vapor new blog-api cd blog-api vapor xcode -y برای اطمینان از صحت کارکرد پروژه ساخته شده، آن را کامپایل کنید. دکمه‌های (CMD + B) را بزنید تا با دیدن پیغام Build successful از این قضیه مطمئن بشیم.ساخت مدل Postفایل جدیدی به نام Post.swift به Xcode در فولدر Sources/App/Models اضافه کنید.مطالب زیر را در فایل وارد کنید. https://gist.github.com/hsharghi/4b04dd98f3075017e643ce5daa5e609e توضیحات قسمت‌های مختلف کد:۱- کتابخانه FluentSQLite برای ساختن مدل هایی هست که به دیتابیس SQLite متصل میشوند.۲- از آنجایی که ORM فریمورک Vapor از دیتابیس های متعدد پشتیبانی میکند و در آن واحد در یک پروژه میشود با چند دیتابیس اتصال برقرار کرد، باید در زمان ساخت مدل مشخص کنیم که این مدل از چه نوع دیتابیسی استفاده میکند.۳- فعلا مدل Post را به ساده ترین حالت تعریف میکنیم. کلاس Post سه پارامتر یا property داره که id یا شناسه یکتا در دیتابیس که از نوع Int است. title یا عنوان و body یا متن که هر دو از نوع String تعریف شده اند. دلیل اینکه id به صورت optional تعریف شده اینه که در زمانی که هنوز Post در دیتابیس ذخیره نشده است مقدار id وجود ندارد، پس باید به صورت optional تعریف بشه.۴- به ساده ترین شکل ممکن یک متد initializer برای کلاس تعریف میکنیم.۵- اضافه کردن امکانات از پیش آماده شده به مدل هایی که در Vapor ساخته میشن به صورت extension به کلاس مورد نظر اضافه میشن. وقتی پروتکل (protocol) از قبل تعریف شده Migration رو به کلاس Post اضافه کنیم در واقع اعلام میکنیم که کلاس Post از این پروتکل پیروی میکنه، و باعث میشه که در هنگام شروع برنامه، جدول مربوط به این مدل به طور خودکار در دیتابیس برنامه ساخته بشه (اگر قبلا ساخته نشده باشه). برای مدل های ساده مثل مدل فعلی ما لازم نیست کدی برای Migration بنویسیم و ساخت جدول به صورت خودکار کفایت میکنه. اما اگر مدل پیچیده‌تر باشه میشه در این قسمت کدهای مربوطه رو نوشت.۶- اگر با Codable در Swift آشنایی داشته باشین، میتونم بهتون بگم که Content در واقع همون Codable هست که چند تا پارامتر به خصوصیات اون اضافه شده. برای کسانی که با Codable آشنا نیستن بگم که کلاسی که از پروتکل Codable پیروی میکنه (conform) به راحتی قابل تبدیل شدن از مدل swift به JSON و بالعکس میباشد. وقتی داریم REST API مینویسیم معلومه که همیشه باید مدل هامون از پروتوکل Content پیروی کنن تا به راحتی تبدیل به JSON و ساخت مدل از روی JSON انجام بشه.۷- وقتی مدل از پروتکل Parameter پیروی کنه، امکان تبدیل خودکار از طریق id مدل، که در url های API فرستاده میشه، به مدل swift رو پیدا میکنه . به این کار Route Model Binding گفته میشه. یعنی اگر چنین آدرسی رو داشته باشیمhttp://my-blog.com/posts/99 هنگامی که این url فرا خونده میشه و متد مربوطه در برنامه ما صدا زده میشه، Vapor به طور خودکار اطلاعات پست با شناسه 99 رو از دیتابیس میخونه، تبدیل به مدل Post میکنه و در اختیار ما قرار میده بدون اینکه ما دستور اضافه ای رو صدا بزنیم.معمولا پیروی مدل‌های Vapor از این ۳ پروتکل برای عمده کارها کفایت میکنه و ما هم فعلا به همین بسنده میکنیم.نوشتن Route های مربوط به Postبرای داشتن یک API کامل (RESTful) برای مدل Post حداقل به ۵ عدد route احتیاج داریم:1- index برای نمایش لیست پست هاMethod: GET | url: /posts2- show برای نمایش یک پست با شناسه مشخص Method: GET | url: /posts/{id}3- create برای ایجاد پست جدیدMethod: POST | url: /posts4- update برای بروزرسانی پست موجود با شناسه مشخصMethod: PATCH | url: /posts/{id}5- delete برای حذف پست موجود با شناسه مشخصMethod: DELETE | url: /posts/{id}فایل routes.swift رو باز کنید و مطالب موجود در اون رو با این مطالب جایگزین کنین. https://gist.github.com/hsharghi/7c05aa9eb47b9ba035f2c75b31c5f9a7 توضیحات قسمت‌های مختلف کد:۱- یک متغیر از روی کلاس PostController ایجاد میکنیم. این کلاس هنوز وجود ندارد و در مرحله بعد اون رو میسازیم برای همین موقتا در Xcdoe خطا مشاهده خواهید کرد. این کلاس شامل تمام توابعی هست که برای نمایش، ساختن، بروزرسانی و پاک کردن پست‌ها لازم است.۲- روت های لازم که در بالا لیست اونها رو دیدیم تعریف میکنیم. دقت کنید Method هر روت مشخص است (get, post, patch, delete) . تمامی پارامتر های این توابع تا قبل از پارامتر use همگی تشکیل دهنده آدرس route هستند. Vapor این پارامترها را به هم متصل میکند تا آدرس URL مربوط به این تابع ساخته شود. مثلا برای اینکه تابعی داشته باشیم تا هنگام فراخوانی درخواست به آدرس زیر صدا زده بشه، /posts/users/5به این صورت نوشته میشهrouter.get(&quotposts&quot, &quotusers&quot, User.parameter, use: UserController.showUserPosts)مطلب مهم در این قسمت استفاده از parameter. برای Route Model Binding است. در کد مربوط به روت های Post چندین جا از Post.paramter استفاده شده. از اونجایی که کلاس Post ما از پروتکل Parameter پیروی میکنه میتونیم از این دستور در route ها استفاده کنیم. این دستور باعث میشه id پست مورد نظر که در URL وجود داره، از دیتابیس خوانده بشه و مدل دریافت شده از دیتابیس در اختیار ما گذاشته بشه. پارامتر دوم تابع router، نام یک تابع از کلاس Controller است که هنگام فراخوانی URL مورد نظر ما اجرا میشود.نوشتن توابع و دستورات PostController فایل جدیدی به اسم PostController.swift در دایرکتوری Sources/App/Controllers اضافه کنید و مطالب زیر را در آن وارد کنید. https://gist.github.com/hsharghi/341f666d86b993b10345e32777f4f5c2 توضیحات قسمت‌های مختلف کد:۱- تابع index برای گرفتن لیست پست‌های موجود و دارای کد ساده‌ای است. تنها کاری که انجام میدهد روی مدل Post یک کوئری میزند و لیست همه Postها را برمیگرداند. از آنجایی که کوئری زدن در Vapor یک عمل Async هست و نتیجه آن بعدا (زمانی که سرور دیتابیس جواب کوئری را برگرداند) مشخص میشود، ما در واقع یک array از پست ها - [Post] - رو برنمیگردونیم و داریم یک Future از نوع [Post] برمیگردونیم. که در این قسمت خود Vapor زحمت این کار رو میکشه و هنگامی که Future<[Post]> l به [Post] تبدیل شد (این قضیه بسته به پیچیدگی کوئری و بار سرور دیتابیس، ممکن است از چند هزارم ثانیه تا چندین ثانیه طول بکشد)، آن را تبدیل به JSON کرده (یادتون باشه که مدل Post از پروتکل Content پیروی میکرد) و نتیجه را به ما برمیگرداند.مطلبی که ممکن است توجه شما را جلب کرده باشه اینه که چرا را متغیر req را به تابع query میفرستیم. به اختصار میشه اینطور گفت که چون عملیات Async در پشت صحنه بسیار پیچیده هستند و احتیاج به یک Worker دارند که متغیر req که از نوع Request میباشد در واقع یک نوع Container هست که درون خود یک یا چند Worker دارد. برای همین ما برای اجرای کوئری ها میتونیم با فرستادن req به تابع query نیاز آن به Worker را تامین کنیم.۲- تابع show برای نمایش جزئیات یک پست استفاده میشه. id پست مورد نظر به عنوان پارامتر در URL فرستاده میشه و از انجایی که مدل Post از پروتکل Parameter پیروی میکنه Vapor میتونه id پست را از داخل URL تشخیص بده و پست مورد نظر رو از دیتابیس بگیره و به ما تحویل بده و کماکان چون این کار با کوئری زدن از دیتابیس انجام میشه جوابی که به دست ما میرسه یک Future از نوع Post هست که ما در انتهای تابع show آن را برمیگردانیم و بقیه کارها را Vapor انجام میدهد (تبدیل Future-Post به Post و پس از آن تبدیل Post به JSON)۳- کار تابع create ذخیره کردن یک Post در دیتابیس است که مشخصات آن در واقع در بدنه ریکوئست (body) فرستاده شده اند. مثل title و body. در این مرحله با توجه به پارامترهای مربوط به پست جدید که در بدنه ریکوئست فرستاده شده‌اند یک مدل Post ساخته میشه و اون رو در متغیر post ذخیره میکنیم. ۴- الان میتونیم تابع save را بر روی متغیر post صدا بزنیم و این مدل را در دیتابیس ذخیره کنیم و در انتها همان مدل ذخیره شده را برمیگردانیم.۵- در تابع delete برای حذف یک پست از دیتابیس همانند تابع show باید id آن پست در URL فرستاده شود. از آنجایی که گرفتن آبجکت post مورد نظر از دیتابیس باعث میشود جواب داده شده Future باشد، و ما میخواهیم عمل delete را بر روی آن اعمال کنیم احتیاج به خود مدل Post داریم نه Future-Post، پس از flatMap استفاده میکنیم تا بتونیم به مدل Post دسترسی داشته باشیم۶- تابع delete رو بر روی آبجکت post صدا میزنیم تا آن را از database حذف کند.۷- معمولا جواب ریکوئست DELETE در صورت موفق بودن یک ریسپانز (HTTP Response) با بدنه خالی و با استتوس 204 است که به نام No-content مشهور مبیاشد. جواب تابع delete را با دستور transform به HTTPStatus.noContent تبدیل میکنیم و آن را برمی‌گردانیم.۸- گرفتن پست موجود که id آن در URL آمده است و استفاده از flatMap برای دسترسی به مدل واقعی Post نه Future-Post۹- آپدیت کردن یک مدل کمی متفاوت است. به ۲ دلیل:‌یکی اینکه مشخصاتی که در بدنه ریکوئست میفرستیم تا برنامه آنها را تغییر دهد لزوما همیشه همه مشخصات موجود در یک مدل نیستند. مثلا در مدل پست ممکن است فقط title رو تغییر بدیم و پارامتر body رو تغییر ندیم. پس فقط پارامتر title با مقدار جدید اون فرستاده می‌شود و اگر بخوایم یک مدل پست با پارامترهای فرستاده شده بسازیم پارامتر body مقدار nil خواد داشت که امکانش نیست. چون همونطور که در مدل Post میبینید پارامتر body به صورت optional تعریف نشده اند. یعنی نمیتونه nil باشند. و دلیل بعدی اینه که شاید بعضی مشخصات پس از ثبت دیگر نبایستی تغییر بکنند. به عنوان مثال بعد از گرفتن مشخصات یک کاربر در هنگام ثبت نام، دیگر امکان تغییر کدملی نباید وجود داشته باشد. پس به هنگام بروزرسانی مشخصات کاربر، اصلا نباید اجازه بدیم در مدلی که از روی پارامترهای بدنه ریکوئست ساخته میشه، پارامتر کدملی وجود داشته باشه. برای رفع این موارد، ما یک مدل شبیه مدل Post میسازیم و پارامترهای اون رو همونطور که مد نظر خودمون هست تعریف میکنیم. برای اینکار یک struct جدید به نام UpdatablePost در کلاس Post تعریف میکنیم و هر دو پارامتر title و body آن رو به صورت optional معرفی میکنیم.final class Post: SQLiteModel {.... // existing code struct UpdatablePost: Content { var title: String? var body: String? }}حالا به جای اینکه بگیم پارامترهای موجود در بدنه ریکوئست رو به مدل Post تبدیل کنه، آنها رو به مدل Post.UpdatablePost تبدیل میکنیم و در متغییر newPostValues ذخیره میکنیم.۱۰- در این قسمت چک میکنیم هر کدام از پارامترهایی که فرستاده شده اند چنانچه مقدار داشتند، مقدار آن را در آبجکت post بروزرسانی میکنیم. هر کدام هم مقداری نداشتند همان مقدار قبلی آن قرارداده میشود. (بدون تغییر می‌ماند)۱۱- تابع update را بر روی post صدا میزنیم تا هر کدام از مقادیر مشخصات آن تغییر کرده بودند در دیتابیس بروزرسانی و ذخیره شوند و در نهایت مدل بروزرسانی شده رو برمیگردانیم. تنظیمات کلی برنامه قبل از اینکه بتونیم کد مون رو کامپایل و اون رو تست کنیم چند مورد رو باید در فایل تنظیمات (config) تغییر بدیم.فایل configure.swift رو باز کنید و مطالب موجود در اون رو با این مطالب جایگزین کنین. https://gist.github.com/hsharghi/b08598e88a8f71d1c6608afeee85e468 ۱- در این قسمت ما نوع ذخیره اطلاعات در دیتابیس sqlite رو در file انتخاب کرده و آدرس و نام فایل مورد نظرمون رو بهش میدیم. برای انجام تست میشه از حالت ذخیره در memory هم استفاده کرد که دیگه احتیاج به وارد کردن نام فایل نداره و پس از هر بار راه اندازی برنامه دیتابیس موجود در memory پاک میشه.۲- در این قسمت به Vapor میگیم که مدل Post رو به لیست migration ها اضافه کن. هر مدلی که به این لیست اضافه بشه در هنگام راه اندازی برنامه چنانچه تیبل اون در دیتابیس ساخته نشده باشه، Vapor اون تیبل رو در دیتابیس ایجاد میکنه. خوب! الان میتونین برنامه رو اجرا کنین. در Xcode از قسمت Scheme گزینه Run -> My Mac رو انتخاب کنید و دکمه Cmd+R رو بزنین و منتظر باشین تا کامپایل تموم بشه و در کنسول Xcode این عبارت رو ببینین.[ INFO ] Migrating 'sqlite' database (.../blog/.build/checkouts/fluent/Sources/Fluent/Migration/MigrationConfig.swift:69) [ INFO ] Preparing migration 'Post' (.../blog/.build/checkouts/fluent/Sources/Fluent/Migration/Migrations.swift:111) [ INFO ] Migrations complete (.../blog/.build/checkouts/fluent/Sources/Fluent/Migration/MigrationConfig.swift:73) Running default command: ~/Library/Developer/Xcode/DerivedData/blog-btnxmupvzrtyhxgiuhlrbbibutsv/Build/Products/Debug/Run serve Server starting on http://localhost:8080الان برنامه شما اجرا شده و روی پورت 8080 منتظر ارسال درخواست های شما هست، میتونین با برنامه REST Client دلخواه تون مثل Paw یا Postman تمامی قسمت های برنامه رو تست کنین. برای تست‌های خیلی ساده و سریع من برنامه Rested رو پیشنهاد میکنم که به رایگان میتونین از Mac Appstore بگیرن. اما برای کارهای پیچیده‌تر یکی از برنامه‌های Paw و Postman و Insomnia پیشنهاد میشه.ضمنا کد این پروژه تا این مرحله در گیت‌هاب موجوده و میتونین اون رو دانلود کنین.در قسمت های بعد به تعریف کاربر و اتصال پست ها به کاربران و نوشتن نظر برای هر پست میپردازیم.نتیجه کارکرد قسمت‌هایی از برنامه رو در این عکسها میتونین ببینینایجاد یک پست جدیدگرفتن لیست پست های ایجاد شدهتغییر title برای پست شماره 2