تقنيات البرمجة المعممة في Rust: كيف ترجمنا Exonum من Iron إلى actix-web

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

تم اختيار Actix-web كبديل للحديد. علاوة على ذلك ، سأخبرنا كيف نقلنا الكود الحالي إلى حل جديد باستخدام تقنيات البرمجة المعممة.


صورة ulleo PD

كيف استخدمنا الحديد


في Exonum ، تم استخدام إطار الحديد دون أي تجريد. لقد قمنا بتثبيت معالجات على موارد معينة ، واستلمنا معلمات الاستعلام عن طريق تحليل عناوين URL باستخدام طرق مساعدة ، وأعادنا النتيجة كسلسلة.

بدا كل شيء شيء من هذا القبيل:

fn set_blocks_response(self, router: &mut Router) { let blocks = move |req: &mut Request| -> IronResult<Response> { let count: usize = self.required_param(req, "count")?; let latest: Option<u64> = self.optional_param(req, "latest")?; let skip_empty_blocks: bool = self.optional_param(req, "skip_empty_blocks")? .unwrap_or(false); let info = self.blocks(count, latest.map(Height), skip_empty_blocks)?; self.ok_response(&::serde_json::to_value(info).unwrap()) }; router.get("/v1/blocks", blocks, "blocks"); } 

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

لماذا كان عليك التخلي عنه


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

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

لماذا تحولنا إلى actix-web


هذا هو إطار عمل شائع يحتل مرتبة عالية في معايير TechEmpower . في الوقت نفسه ، على عكس الحديد ، يتطور بنشاط. تتمتع Actix-web بواجهة برمجة تطبيقات جيدة التصميم وتنفيذ عالي الجودة استنادًا إلى إطار عمل actix. تتم معالجة الطلبات بشكل غير متزامن من قبل تجمع مؤشرات الترابط ، وإذا أدت المعالجة إلى الذعر ، تتم إعادة تشغيل الممثل تلقائيًا.

بالطبع ، تحتوي أكتيكس ويب على عيوب ، على سبيل المثال ، تحتوي على كمية كبيرة من التعليمات البرمجية غير الآمنة. ولكن تم إعادة كتابته لاحقًا في Safe Rust ، والتي حلت هذه المشكلة.

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

ما نريده من إطار ويب


كان من المهم بالنسبة لنا ليس فقط تغيير Iron إلى actix-web ، ولكن لإنشاء أساس للمستقبل - لوضع بنية API جديدة للتلخيص من إطار ويب معين. سيسمح لك ذلك بإنشاء معالجات ، تقريبًا دون التفكير في تفاصيل الويب ونقلها إلى أي خلفية. يمكن القيام بذلك عن طريق كتابة واجهة أمامية تعمل على الأنواع والأنواع الأساسية.

لفهم كيف تبدو هذه الواجهة الأمامية ، دعنا نحدد أي API HTTP:

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

إذا قمنا بتحليل جميع طبقات التجريد ، فقد اتضح أن أي طلب HTTP هو مجرد استدعاء دالة:

 fn request(context: &ServiceContext, query: Query) -> Result<Response, ServiceError> 

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

سمة نقطة النهاية لمعالجة طلب HTTP المعمم

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

 // ,   GET .      //    ,      . //         . trait Endpoint: Sync + Send + 'static { type Request: DeserializeOwned + 'static; type Response: Serialize + 'static; fn handle(&self, context: &Context, request: Self::Request) -> Result<Self::Response, io::Error>; } 

بعد ذلك ، ستحتاج إلى تنفيذ هذا المعالج في إطار عمل محدد. لنفترض أنه بالنسبة إلى actix-web ، يبدو الأمر كالتالي:

 //    actix-web.  ,   , //  `Endpoint`   . type FutureResponse = actix_web::FutureResponse<HttpResponse, actix_web::Error>; // «»    actix-web.      //   .     , //     . type RawHandler = dyn Fn(HttpRequest<Context>) -> FutureResponse + 'static + Send + Sync; //   ,     ,     . #[derive(Clone)] struct RequestHandler { ///  . pub name: String, /// HTTP . pub method: actix_web::http::Method, ///  .  ,       . pub inner: Arc<RawHandler>, } 

يمكنك استخدام الهياكل لتمرير معلمات الطلب من خلال السياق. يمكن لـ Actix-web إلغاء تسلسل المعلمات تلقائيًا باستخدام serde. على سبيل المثال ، يتم إلغاء تسلسل a = 15 & b = hello إلى هيكل من النموذج التالي:

 #[derive(Deserialize)] struct SimpleQuery { a: i32, b: String, } 

يتوافق هذا مع نوع الطلب المرتبط من سمة نقطة النهاية.

الآن دعنا نكتب محولًا يلف تطبيق نقطة النهاية محددًا في RequstHandler لـ actix-web. يرجى ملاحظة أن المعلومات حول أنواع الطلب والاستجابة يتم فقدها في هذه العملية. هذه التقنية تسمى محو النوع. وتتمثل مهمتها في تحويل الجدولة الثابتة إلى ديناميكية.

 impl RequestHandler { fn from_endpoint<E: Endpoint>(name: &str, endpoint: E) -> RequestHandler { let index = move |request: HttpRequest<Context>| -> FutureResponse { let context = request.state(); let future = Query::from_request(&request, &()) .map(|query: Query<E::Request>| query.into_inner()) .and_then(|query| endpoint.handle(context, query).map_err(From::from)) .and_then(|value| Ok(HttpResponse::Ok().json(value))) .into_future(); Box::new(future) }; Self { name: name.to_owned(), method: actix_web::http::Method::GET, inner: Arc::from(index) as Arc<RawHandler>, } } } 

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

اكتب المشاكل

عند كتابة معالج ، يتم إنشاء الكثير من التعليمات البرمجية المساعدة:

 //    . struct ElementCountEndpoint { elements: Rc<RefCell<Vec<Something>>>, } //   Endpoint. impl Endpoint for ElementCountEndpoint { type Request = (); type Result = usize; fn handle(&self, context: &Context, _request: ()) -> Result<usize, io::Error> { Ok(self.elements.borrow().len()) } } //    . let endpoint = ElementCountEndpoint::new(elements.clone()); let handler = RequestHandler::from_endpoint("/v1/element_count", endpoint); actix_backend.endpoint(handler); 

من الناحية المثالية ، أود أن أتمكن من تمرير إغلاق عادي كمعالج ، مما يقلل من مقدار ضوضاء بناء الجملة بترتيب من الحجم:

 let elements = elements.clone(); actix_backend.endpoint("/v1/elements_count", move || {   Ok(elements.borrow().len()) }); 

سأتحدث عن كيفية القيام بذلك لاحقًا.

غمر سهل في البرمجة العامة


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

يمكن أن تكون الحجج ونتائج الإغلاق من أنواع مختلفة ، لذلك عليك العمل مع طريقة التحميل الزائد. الصدأ لا يدعم التحميل الزائد بشكل مباشر ، ولكنه يسمح بمحاكاة ذلك باستخدام سمات Into و From.

بالإضافة إلى ذلك ، لا يجب أن يتطابق نوع القيمة المرجعة للإغلاق مع القيمة المرجعة لتطبيق نقطة النهاية. لمعالجة هذا النوع ، يجب استخراجه من نوع الإغلاق المستلم.

استخراج الأنواع من نوع Fn

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

الفكرة الرئيسية هي استخدام بنية مساعدة من النموذج التالي:

 ///       F: Fn(A) -> B. struct SimpleExtractor<A, B, F> {   //   .   inner: F,   _a: PhantomData<A>,   _b: PhantomData<B>, } 

نحن مضطرون لاستخدام PhantomData لأن Rust يتطلب أن تكون جميع معلمات التعميم في تعريف الهيكل. ومع ذلك ، لا يتم تعميم النوع المحدد للإغلاق أو الوظيفة F (على الرغم من أنها تطبق النوع المعمم Fn). لا يتم استخدام معلمات الكتابة A و B بشكل مباشر فيها.

هذا القيد لنظام الصدأ الذي لا يسمح باستخدام استراتيجية أبسط - لتطبيق سمة نقطة الإغلاق مباشرة:

 impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B { type Request = A; type Response = B; fn handle(&self, context: &Context, request: A) -> Result<B, io::Error> { // ... } } 

المحول البرمجي في هذه الحالة يعرض خطأ:

 error[E0207]: the type parameter `A` is not constrained by the impl trait, self type, or predicates --> src/main.rs:10:6 | 10 | impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B { | ^ unconstrained type parameter 

يجعل الهيكل المساعد لـ SimpleExtractor من الممكن وصف التحويل من. يسمح لك بحفظ أي وظيفة واستخراج أنواع حججها:

 impl<A, B, F> From<F> for SimpleExtractor<A, B, F> where F: Fn(&Context, A) -> B, A: DeserializeOwned, B: Serialize, { fn from(inner: F) -> Self { SimpleExtractor { inner, _a: PhantomData, _b: PhantomData, } } } 

يتم تجميع التعليمات البرمجية التالية بنجاح:

 #[derive(Deserialize)] struct Query { a: i32, b: String, }; //   . fn my_handler(_: &Context, q: Query) -> String { format!("{} has {} apples.", qb, qa) } let fn_extractor = SimpleExtractor::from(my_handler); //  . let c = 15; let my_closure = |_: &Context, q: Query| -> String { format!("{} has {} apples, but Alice has {}", qb, qa, c) }; let closure_extractor = SimpleExtractor::from(my_closure); 

أنواع التخصص والعلامات

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

أولاً ، نعيد كتابة SimpleExtractor حتى يتمكن من التمييز بين النتائج المتزامنة وغير المتزامنة. في نفس الوقت ، نقوم بتطبيق سمة "من" لكل حالة من الحالات. لاحظ أنه يمكن تنفيذ السمات لمتغيرات محددة من الهياكل المعممة.

 ///   HTTP-. pub struct With<Q, I, R, F> { ///  -. pub handler: F, ///     . _query_type: PhantomData<Q>, ///   . _item_type: PhantomData<I>, ///  ,  . ///  ,       . _result_type: PhantomData<R>, } //   ,   . impl<Q, I, F> From<F> for With<Q, I, Result<I>, F> where F: Fn(&ServiceApiState, Q) -> Result<I>, { fn from(handler: F) -> Self { Self { handler, _query_type: PhantomData, _item_type: PhantomData, _result_type: PhantomData, } } } //     . impl<Q, I, F> From<F> for With<Q, I, FutureResult<I>, F> where F: Fn(&ServiceApiState, Q) -> FutureResult<I>, { fn from(handler: F) -> Self { Self { handler, _query_type: PhantomData, _item_type: PhantomData, _result_type: PhantomData, } } } 

نحتاج الآن إلى التصريح عن هيكل يتم فيه دمج معالج الطلب باسمه وتنوعه:

 #[derive(Debug)] pub struct NamedWith<Q, I, R, F, K> {   ///  .   pub name: String,   ///    .   pub inner: With<Q, I, R, F>,   ///  .   _kind: PhantomData<K>, } 

بعد أن يمكنك الإعلان عن العديد من الهياكل الفارغة التي ستكون بمثابة أنواع علامات. تسمح لك العلامات بتطبيق رمز التحويل الخاص بكل معالج من معالجات RequestHandler الموصوفة سابقًا.

 /// ,    .  HTTP   GET-. pub struct Immutable; /// ,   .  HTTP   POST, PUT, UPDATE ///    ,        POST. pub struct Mutable; 

الآن يمكننا تحديد أربعة تطبيقات مختلفة للنوع من جميع تركيبات معلمات قالب R و K ​​(قيمة الإرجاع للمعالج ونوع الطلب).

 //     get . impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Immutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, Result<I>, F, Immutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let context = request.state(); let future = Query::from_request(&request, &()) .map(|query: Query<Q>| query.into_inner()) .and_then(|query| handler(context, query).map_err(From::from)) .and_then(|value| Ok(HttpResponse::Ok().json(value))) .into_future(); Box::new(future) }; Self { name: f.name, method: actix_web::http::Method::GET, inner: Arc::from(index) as Arc<RawHandler>, } } } //     post . impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Mutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, Result<I>, F, Mutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let handler = handler.clone(); let context = request.state().clone(); request .json() .from_err() .and_then(move |query: Q| { handler(&context, query) .map(|value| HttpResponse::Ok().json(value)) .map_err(From::from) }) .responder() }; Self { name: f.name, method: actix_web::http::Method::POST, inner: Arc::from(index) as Arc<RawHandler>, } } } //     get . impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Immutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, FutureResult<I>, F, Immutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let context = request.state().clone(); let handler = handler.clone(); Query::from_request(&request, &()) .map(move |query: Query<Q>| query.into_inner()) .into_future() .and_then(move |query| handler(&context, query).map_err(From::from)) .map(|value| HttpResponse::Ok().json(value)) .responder() }; Self { name: f.name, method: actix_web::http::Method::GET, inner: Arc::from(index) as Arc<RawHandler>, } } } //     post . impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Mutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, FutureResult<I>, F, Mutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let handler = handler.clone(); let context = request.state().clone(); request .json() .from_err() .and_then(move |query: Q| { handler(&context, query) .map(|value| HttpResponse::Ok().json(value)) .map_err(From::from) }) .responder() }; Self { name: f.name, method: actix_web::http::Method::POST, inner: Arc::from(index) as Arc<RawHandler>, } } } 

"الواجهة" للواجهة الخلفية

الآن كل هذا يبقى لكتابة "واجهة" ، والتي ستأخذ عمليات الإغلاق وتضيفها إلى الواجهة الخلفية المقابلة. في حالتنا ، هناك خلفية واحدة فقط - actix-web - ولكن خلف الواجهة يمكنك إخفاء أي تطبيقات إضافية تريدها ، على سبيل المثال ، مولد لمواصفات Swagger.

 pub struct ServiceApiScope { actix_backend: actix::ApiBuilder, } impl ServiceApiScope { ///    Immutable    . pub fn endpoint<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self where //     ,      : Q: DeserializeOwned + 'static, I: Serialize + 'static, F: Fn(&ServiceApiState, Q) -> R + 'static + Clone, E: Into<With<Q, I, R, F>>, //  ,          //  NamedWith  RequestHandler. RequestHandler: From<NamedWith<Q, I, R, F, Immutable>>, { self.actix_backend.endpoint(name, endpoint); self } ///    Mutable . pub fn endpoint_mut<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self where Q: DeserializeOwned + 'static, I: Serialize + 'static, F: Fn(&ServiceApiState, Q) -> R + 'static + Clone, E: Into<With<Q, I, R, F>>, RequestHandler: From<NamedWith<Q, I, R, F, Mutable>>, { self.actix_backend.endpoint_mut(name, endpoint); self } 

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

مساوئ النهج


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

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

 impl<(), I, F> From<F> for With<(), I, Result<I>, F> where F: Fn(&ServiceApiState) -> Result<I>, { fn from(handler: F) -> Self { Self { handler, _query_type: PhantomData, _item_type: PhantomData, _result_type: PhantomData, } } } 

ونتيجة لذلك ، يجب أن تقبل الطلبات التي لا تحتوي على معلمات سلسلة JSON فارغة ، والتي تم إلغاء تسلسلها إلى (). يمكن حل هذه المشكلة من خلال التخصص في أسلوب C ++ ، ولكنها حتى الآن لا تتوفر إلا في الإصدار الليلي من المترجم وليس من الواضح متى "تستقر".

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

يفرض استعلام فك تشفير عنوان URL في طلبات GET أيضًا بعض القيود غير الواضحة على نوع المعلمات ، ولكن هذه بالفعل ميزات لتنفيذ serde-urlencoded.

الخلاصة


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

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


All Articles