picodata_plugin/transport/
context.rs

1use crate::util::DisplayAsHexBytesLimitted;
2use crate::util::DisplayErrorLocation;
3use crate::util::FfiSafeBytes;
4use crate::util::FfiSafeStr;
5use std::borrow::Cow;
6use std::cell::OnceCell;
7use std::collections::HashMap;
8use std::io::Cursor;
9use tarantool::error::BoxError;
10use tarantool::error::TarantoolErrorCode;
11use tarantool::error::TarantoolErrorCode::InvalidMsgpack;
12use tarantool::ffi::uuid::tt_uuid;
13use tarantool::msgpack::skip_value;
14use tarantool::unwrap_ok_or;
15use tarantool::uuid::Uuid;
16
17////////////////////////////////////////////////////////////////////////////////
18// Context
19////////////////////////////////////////////////////////////////////////////////
20
21/// Context stores request metadata. This includes some builtin fields, for example:
22/// - [`Context::request_id`],
23/// - [`Context::path`],
24/// - [`Context::plugin_name`],
25/// - [`Context::plugin_version`],
26/// - [`Context::service_name`],
27///
28/// But also supports arbitrary user defined named fields.
29pub struct Context<'a> {
30    request_id: Uuid,
31    path: &'a str,
32    plugin_name: &'a str,
33    service_name: &'a str,
34    plugin_version: &'a str,
35
36    /// Raw context encoded as msgpack.
37    raw: &'a [u8],
38
39    /// Custom context fields string keys. This field is constructed on demand.
40    named_fields: OnceCell<Result<ContextNamedFields<'a>, BoxError>>,
41}
42
43type ContextNamedFields<'a> = HashMap<Cow<'a, str>, ContextValue<'a>>;
44
45impl<'a> Context<'a> {
46    #[inline]
47    pub(crate) fn new(context: &'a FfiSafeContext) -> Self {
48        // SAFETY: these are all safe because lifetime is remembered
49        let path = unsafe { context.path.as_str() };
50        let plugin_name = unsafe { context.plugin_name.as_str() };
51        let service_name = unsafe { context.service_name.as_str() };
52        let plugin_version = unsafe { context.plugin_version.as_str() };
53        let raw = unsafe { context.raw.as_bytes() };
54        Self {
55            request_id: Uuid::from_tt_uuid(context.request_id),
56            path,
57            plugin_name,
58            service_name,
59            plugin_version,
60            raw,
61            named_fields: OnceCell::new(),
62        }
63    }
64
65    #[inline(always)]
66    pub fn request_id(&self) -> Uuid {
67        self.request_id
68    }
69
70    #[inline(always)]
71    pub fn path(&self) -> &str {
72        self.path
73    }
74
75    #[inline(always)]
76    pub fn plugin_name(&self) -> &str {
77        self.plugin_name
78    }
79
80    #[inline(always)]
81    pub fn service_name(&self) -> &str {
82        self.service_name
83    }
84
85    #[inline(always)]
86    pub fn plugin_version(&self) -> &str {
87        self.plugin_version
88    }
89
90    #[inline(always)]
91    pub fn raw(&self) -> &[u8] {
92        self.raw
93    }
94
95    /// Set a named `field` to the `value`. Returns the old value if it was set.
96    #[inline(always)]
97    pub fn set(
98        &mut self,
99        field: impl Into<Cow<'static, str>>,
100        value: impl Into<ContextValue<'static>>,
101    ) -> Result<Option<ContextValue<'a>>, BoxError> {
102        // Make sure the map is initialized.
103        // TODO: use `OnceCell::get_mut_or_init` when it's stable.
104        self.get_named_fields()?;
105
106        let res: &mut Result<ContextNamedFields, BoxError> = self
107            .named_fields
108            .get_mut()
109            .expect("just made sure it's there");
110        let named_fields: &mut ContextNamedFields = res
111            .as_mut()
112            .expect("if it was an error we would've returned early");
113        let old_value = named_fields.insert(field.into(), value.into());
114        Ok(old_value)
115    }
116
117    #[inline(always)]
118    pub fn get(&self, field: &str) -> Result<Option<&ContextValue<'a>>, BoxError> {
119        let named_fields = self.get_named_fields()?;
120        Ok(named_fields.get(field))
121    }
122
123    #[inline]
124    pub fn get_named_fields(&self) -> Result<&ContextNamedFields<'a>, BoxError> {
125        self.named_fields
126            .get_or_init(|| decode_msgpack_string_fields(self.raw))
127            .as_ref()
128            .map_err(Clone::clone)
129    }
130}
131
132impl std::fmt::Debug for Context<'_> {
133    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
134        let mut s = f.debug_struct("Context");
135        let mut s = s
136            .field("request_id", &DebugUuid(self.request_id))
137            .field("path", &self.path)
138            .field("plugin_name", &self.plugin_name)
139            .field("service_name", &self.service_name)
140            .field("plugin_version", &self.plugin_version);
141        if let Ok(named_fields) = self.get_named_fields() {
142            for (key, value) in named_fields {
143                s = s.field(key, value);
144            }
145            s.finish()
146        } else {
147            s.finish_non_exhaustive()
148        }
149    }
150}
151
152tarantool::define_enum_with_introspection! {
153    /// Context integer field identifiers (keys in the msgpack mapping).
154    pub enum ContextFieldId {
155        RequestId = 1,
156        PluginName = 2,
157        ServiceName = 3,
158        PluginVersion = 4,
159    }
160}
161
162#[inline]
163fn decode_msgpack_string_fields<'a>(raw: &'a [u8]) -> Result<ContextNamedFields<'a>, BoxError> {
164    decode_msgpack_string_fields_impl(raw).map_err(|e| log_context_decoding_error(raw, e))
165}
166
167fn decode_msgpack_string_fields_impl<'a>(
168    raw: &'a [u8],
169) -> Result<ContextNamedFields<'a>, BoxError> {
170    let mut buffer = Cursor::new(raw);
171
172    let count = rmp::decode::read_map_len(&mut buffer).map_err(invalid_msgpack)? as usize;
173
174    let mut named_fields = ContextNamedFields::with_capacity(count);
175    for i in 0..count {
176        let Ok(marker) = rmp::decode::read_marker(&mut buffer) else {
177            #[rustfmt::skip]
178            return Err(BoxError::new(InvalidMsgpack, format!("not enough values in mapping: expected {count}, got {i}")));
179        };
180        if let Some(_field_id) = read_rest_of_uint(marker, &mut buffer)? {
181            // Integer field id
182            // Skip as it should've already been handled the first time around
183            skip_value(&mut buffer).map_err(invalid_msgpack)?;
184        } else if let Some(field_name) = read_rest_of_str(marker, &mut buffer)? {
185            // String field name
186            let value = rmp_serde::decode::from_read(&mut buffer)
187                .map_err(|e| decoding_field(field_name, invalid_msgpack(e)))?;
188            named_fields.insert(Cow::Borrowed(field_name), value);
189        } else {
190            // Invalid field key
191            #[rustfmt::skip]
192            return Err(BoxError::new(InvalidMsgpack, format!("context field must be positive integer or string, got {marker:?}")));
193        }
194    }
195
196    Ok(named_fields)
197}
198
199////////////////////////////////////////////////////////////////////////////////
200// ContextValue
201////////////////////////////////////////////////////////////////////////////////
202
203#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
204#[serde(untagged)]
205pub enum ContextValue<'a> {
206    Bool(bool),
207    Int(i64),
208    Float(f64),
209    String(Cow<'a, str>),
210    Array(Vec<ContextValue<'a>>),
211}
212
213impl ContextValue<'_> {
214    #[inline(always)]
215    pub fn bool(&self) -> Option<bool> {
216        match self {
217            &Self::Bool(v) => Some(v),
218            _ => None,
219        }
220    }
221
222    #[inline(always)]
223    pub fn int(&self) -> Option<i64> {
224        match self {
225            &Self::Int(v) => Some(v),
226            _ => None,
227        }
228    }
229
230    #[inline(always)]
231    pub fn float(&self) -> Option<f64> {
232        match self {
233            &Self::Float(v) => Some(v),
234            &Self::Int(v) => Some(v as _),
235            _ => None,
236        }
237    }
238
239    #[inline(always)]
240    pub fn str(&self) -> Option<&str> {
241        match self {
242            Self::String(v) => Some(v),
243            _ => None,
244        }
245    }
246
247    #[inline(always)]
248    pub fn array(&self) -> Option<&[Self]> {
249        match self {
250            Self::Array(v) => Some(&*v),
251            _ => None,
252        }
253    }
254
255    /// Returns true if `&self` does not contain any `&str`.
256    ///
257    /// See also [`Self::into_owned`].
258    pub fn is_owned(&self) -> bool {
259        match self {
260            Self::Bool { .. } => true,
261            Self::Int { .. } => true,
262            Self::Float { .. } => true,
263            Self::String(v) => matches!(v, Cow::Owned { .. }),
264            Self::Array(v) => v.iter().all(Self::is_owned),
265        }
266    }
267
268    /// Converts `self` into a version with a `'static` lifetime. This means
269    /// that any `&str` stored inside will be converted to `String`.
270    pub fn into_owned(self) -> ContextValue<'static> {
271        match self {
272            Self::Bool(v) => ContextValue::Bool(v),
273            Self::Int(v) => ContextValue::Int(v),
274            Self::Float(v) => ContextValue::Float(v),
275            // Note: even though this could be Cow::Borrowed(&'static str) which already has 'static lifetime,
276            // rust will not less us know this information, so we must do a redundant allocation.
277            // Let's just wait until "specialization" is stabilized (which will never happen by the way).
278            Self::String(v) => ContextValue::String(Cow::Owned(v.into())),
279            Self::Array(v) => ContextValue::Array(v.into_iter().map(Self::into_owned).collect()),
280        }
281    }
282}
283
284impl From<bool> for ContextValue<'_> {
285    #[inline(always)]
286    fn from(v: bool) -> Self {
287        Self::Bool(v)
288    }
289}
290
291#[rustfmt::skip]
292pub mod impl_int {
293    use super::*;
294    impl From<i8> for ContextValue<'_> { #[inline(always)] fn from(v: i8) -> Self { Self::Int(v as _) } }
295    impl From<i16> for ContextValue<'_> { #[inline(always)] fn from(v: i16) -> Self { Self::Int(v as _) } }
296    impl From<i32> for ContextValue<'_> { #[inline(always)] fn from(v: i32) -> Self { Self::Int(v as _) } }
297    impl From<i64> for ContextValue<'_> { #[inline(always)] fn from(v: i64) -> Self { Self::Int(v as _) } }
298    impl From<isize> for ContextValue<'_> { #[inline(always)] fn from(v: isize) -> Self { Self::Int(v as _) } }
299    impl From<u8> for ContextValue<'_> { #[inline(always)] fn from(v: u8) -> Self { Self::Int(v as _) } }
300    impl From<u16> for ContextValue<'_> { #[inline(always)] fn from(v: u16) -> Self { Self::Int(v as _) } }
301    impl From<u32> for ContextValue<'_> { #[inline(always)] fn from(v: u32) -> Self { Self::Int(v as _) } }
302    impl From<u64> for ContextValue<'_> { #[inline(always)] fn from(v: u64) -> Self { Self::Int(v as _) } }
303    impl From<usize> for ContextValue<'_> { #[inline(always)] fn from(v: usize) -> Self { Self::Int(v as _) } }
304}
305
306impl From<f32> for ContextValue<'_> {
307    #[inline(always)]
308    fn from(v: f32) -> Self {
309        Self::Float(v as _)
310    }
311}
312
313impl From<f64> for ContextValue<'_> {
314    #[inline(always)]
315    fn from(v: f64) -> Self {
316        Self::Float(v)
317    }
318}
319
320impl<'a> From<&'a str> for ContextValue<'a> {
321    #[inline(always)]
322    fn from(s: &'a str) -> Self {
323        Self::String(s.into())
324    }
325}
326
327impl From<String> for ContextValue<'_> {
328    #[inline(always)]
329    fn from(s: String) -> Self {
330        Self::String(s.into())
331    }
332}
333
334impl<'a> From<Cow<'a, str>> for ContextValue<'a> {
335    #[inline(always)]
336    fn from(s: Cow<'a, str>) -> Self {
337        Self::String(s)
338    }
339}
340
341////////////////////////////////////////////////////////////////////////////////
342// FfiSafeContext
343////////////////////////////////////////////////////////////////////////////////
344
345/// **For internal use**.
346///
347/// Use [`Context`] instead.
348#[derive(Copy, Clone)]
349#[repr(C)]
350pub struct FfiSafeContext {
351    pub request_id: tt_uuid,
352    pub path: FfiSafeStr,
353    pub plugin_name: FfiSafeStr,
354    pub service_name: FfiSafeStr,
355    pub plugin_version: FfiSafeStr,
356
357    /// Raw context encoded as msgpack.
358    raw: FfiSafeBytes,
359}
360
361impl FfiSafeContext {
362    #[inline]
363    pub fn for_local_call(
364        request_id: Uuid,
365        path: &str,
366        plugin: &str,
367        service: &str,
368        version: &str,
369        raw: &[u8],
370    ) -> Self {
371        Self {
372            request_id: request_id.to_tt_uuid(),
373            path: path.into(),
374            plugin_name: plugin.into(),
375            service_name: service.into(),
376            plugin_version: version.into(),
377
378            raw: raw.into(),
379        }
380    }
381
382    #[inline(always)]
383    pub fn decode_msgpack(path: &str, raw: &[u8]) -> Result<Self, ()> {
384        Self::decode_msgpack_impl(path, raw)
385            // Note: error is passed via box_error_last, because plugin may have
386            // a different version of tarantool-module than picodata
387            .map_err(|e| e.set_last())
388    }
389
390    pub fn decode_msgpack_impl(path: &str, raw: &[u8]) -> Result<Self, BoxError> {
391        let mut request_id = None;
392        let mut plugin_name = None;
393        let mut service_name = None;
394        let mut plugin_version = None;
395
396        let mut buffer = Cursor::new(raw);
397        let count = unwrap_ok_or!(rmp::decode::read_map_len(&mut buffer),
398            Err(e) => return Err(BoxError::new(InvalidMsgpack, format!("expected a map: {e}")))
399        );
400        for i in 0..count {
401            let Ok(marker) = rmp::decode::read_marker(&mut buffer) else {
402                #[rustfmt::skip]
403                return Err(BoxError::new(InvalidMsgpack, format!("not enough entries in mapping: expected {count}, got {i}")));
404            };
405            if let Some(field_id) = read_rest_of_uint(marker, &mut buffer)? {
406                // Integer field id
407                match ContextFieldId::try_from(field_id) {
408                    Ok(field @ ContextFieldId::RequestId) => {
409                        #[rustfmt::skip]
410                        let v: Uuid = rmp_serde::decode::from_read(&mut buffer).map_err(|e| decoding_field(&field, invalid_msgpack(e)))?;
411                        request_id = Some(v);
412                    }
413                    Ok(field @ ContextFieldId::PluginName) => {
414                        #[rustfmt::skip]
415                        let v = read_str(&mut buffer).map_err(|e| decoding_field(&field, e))?;
416                        plugin_name = Some(v);
417                    }
418                    Ok(field @ ContextFieldId::ServiceName) => {
419                        #[rustfmt::skip]
420                        let v = read_str(&mut buffer).map_err(|e| decoding_field(&field, e))?;
421                        service_name = Some(v);
422                    }
423                    Ok(field @ ContextFieldId::PluginVersion) => {
424                        #[rustfmt::skip]
425                        let v = read_str(&mut buffer).map_err(|e| decoding_field(&field, e))?;
426                        plugin_version = Some(v);
427                    }
428                    Err(unknown_field_id) => {
429                        #[rustfmt::skip]
430                        tarantool::say_verbose!("ignoring unknown context field with integer id {unknown_field_id} (0x{unknown_field_id:02x})");
431                        skip_value(&mut buffer).map_err(invalid_msgpack)?;
432                    }
433                }
434            } else if let Some(_field_name) = read_rest_of_str(marker, &mut buffer)? {
435                // String field name
436                // Skip for now, will be handled later if user requests it
437                skip_value(&mut buffer).map_err(invalid_msgpack)?;
438            } else {
439                // Invalid field key
440                #[rustfmt::skip]
441                return Err(BoxError::new(InvalidMsgpack, format!("context field must be positive integer or string, got {marker:?}")));
442            }
443        }
444
445        let end_index = buffer.position() as usize;
446        if end_index > raw.len() {
447            #[rustfmt::skip]
448            return Err(BoxError::new(InvalidMsgpack, format!("expected more data after {}", DisplayAsHexBytesLimitted(raw))));
449        } else if end_index < raw.len() {
450            let tail = &raw[end_index..];
451            #[rustfmt::skip]
452            return Err(BoxError::new(InvalidMsgpack, format!("unexpected data after context: {}", DisplayAsHexBytesLimitted(tail))));
453        }
454
455        let Some(request_id) = request_id else {
456            return Err(invalid_msgpack("context must contain a request_id"));
457        };
458
459        let Some(plugin_name) = plugin_name else {
460            return Err(invalid_msgpack("context must contain a plugin_name"));
461        };
462
463        let Some(service_name) = service_name else {
464            return Err(invalid_msgpack("context must contain a service_name"));
465        };
466
467        let Some(plugin_version) = plugin_version else {
468            return Err(invalid_msgpack("context must contain a plugin_version"));
469        };
470
471        Ok(Self {
472            request_id: request_id.to_tt_uuid(),
473            path: path.into(),
474            plugin_name: plugin_name.into(),
475            service_name: service_name.into(),
476            plugin_version: plugin_version.into(),
477            raw: raw.into(),
478        })
479    }
480}
481
482impl std::fmt::Debug for FfiSafeContext {
483    #[inline]
484    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
485        f.debug_struct("FfiSafeContext")
486            .field(
487                "request_id",
488                &DebugUuid(Uuid::from_tt_uuid(self.request_id)),
489            )
490            .field("path", &self.path)
491            .field("plugin_name", &self.plugin_name)
492            .field("service_name", &self.service_name)
493            .field("plugin_version", &self.plugin_version)
494            .finish_non_exhaustive()
495    }
496}
497
498////////////////////////////////////////////////////////////////////////////////
499// miscellaneous
500////////////////////////////////////////////////////////////////////////////////
501
502fn read_str<'a>(buffer: &mut Cursor<&'a [u8]>) -> Result<&'a str, BoxError> {
503    let length = rmp::decode::read_str_len(buffer).map_err(invalid_msgpack)? as usize;
504
505    str_from_cursor(length, buffer)
506}
507
508fn read_rest_of_str<'a>(
509    marker: rmp::Marker,
510    buffer: &mut Cursor<&'a [u8]>,
511) -> Result<Option<&'a str>, BoxError> {
512    use rmp::decode::RmpRead as _;
513
514    let length;
515    match marker {
516        rmp::Marker::FixStr(v) => {
517            length = v as usize;
518        }
519        rmp::Marker::Str8 => {
520            length = buffer.read_data_u8().map_err(invalid_msgpack)? as usize;
521        }
522        rmp::Marker::Str16 => {
523            length = buffer.read_data_u16().map_err(invalid_msgpack)? as usize;
524        }
525        rmp::Marker::Str32 => {
526            length = buffer.read_data_u32().map_err(invalid_msgpack)? as usize;
527        }
528        _ => return Ok(None),
529    }
530
531    str_from_cursor(length, buffer).map(Some)
532}
533
534#[inline]
535fn str_from_cursor<'a>(length: usize, buffer: &mut Cursor<&'a [u8]>) -> Result<&'a str, BoxError> {
536    let start_index = buffer.position() as usize;
537    let data = *buffer.get_ref();
538    let remaining_length = data.len() - start_index;
539    if remaining_length < length {
540        return Err(BoxError::new(
541            TarantoolErrorCode::InvalidMsgpack,
542            format!("expected a string of length {length}, got {remaining_length}"),
543        ));
544    }
545
546    let end_index = start_index + length;
547    let res = std::str::from_utf8(&data[start_index..end_index]).map_err(invalid_msgpack)?;
548    buffer.set_position(end_index as _);
549    Ok(res)
550}
551
552fn read_rest_of_uint(
553    marker: rmp::Marker,
554    buffer: &mut Cursor<&[u8]>,
555) -> Result<Option<u64>, BoxError> {
556    use rmp::decode::RmpRead as _;
557    match marker {
558        rmp::Marker::FixPos(v) => Ok(Some(v as _)),
559        rmp::Marker::U8 => {
560            let v = buffer.read_data_u8().map_err(invalid_msgpack)?;
561            Ok(Some(v as _))
562        }
563        rmp::Marker::U16 => {
564            let v = buffer.read_data_u16().map_err(invalid_msgpack)?;
565            Ok(Some(v as _))
566        }
567        rmp::Marker::U32 => {
568            let v = buffer.read_data_u32().map_err(invalid_msgpack)?;
569            Ok(Some(v as _))
570        }
571        rmp::Marker::U64 => {
572            let v = buffer.read_data_u64().map_err(invalid_msgpack)?;
573            Ok(Some(v as _))
574        }
575        _ => Ok(None),
576    }
577}
578
579#[track_caller]
580fn log_context_decoding_error(raw: &[u8], e: BoxError) -> BoxError {
581    let location = DisplayErrorLocation(&e);
582    if raw.len() <= 512 {
583        let raw = DisplayAsHexBytesLimitted(raw);
584        tarantool::say_error!("failed to decode context from msgpack ({raw}): {location}{e}");
585    } else {
586        tarantool::say_error!(
587            "failed to decode context from msgpack (too big to display): {location}{e}"
588        );
589    }
590    e
591}
592
593#[inline(always)]
594#[track_caller]
595fn decoding_field(field: &(impl std::fmt::Debug + ?Sized), error: BoxError) -> BoxError {
596    BoxError::new(
597        error.error_code(),
598        format!("failed decoding field {field:?}: {}", error.message()),
599    )
600}
601
602#[inline(always)]
603#[track_caller]
604fn invalid_msgpack(error: impl ToString) -> BoxError {
605    BoxError::new(TarantoolErrorCode::InvalidMsgpack, error.to_string())
606}
607
608// TODO: fix impl Debug for Uuid in tarantool-module https://git.picodata.io/picodata/picodata/tarantool-module/-/issues/209
609struct DebugUuid(Uuid);
610impl std::fmt::Debug for DebugUuid {
611    #[inline(always)]
612    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
613        write!(f, "{}", self.0)
614    }
615}
616
617////////////////////////////////////////////////////////////////////////////////
618// tests
619////////////////////////////////////////////////////////////////////////////////
620
621#[cfg(feature = "internal_test")]
622mod tests {
623    use super::*;
624
625    #[tarantool::test]
626    fn decode_context() {
627        //
628        // Check error cases
629        //
630        let e = FfiSafeContext::decode_msgpack_impl("path", b"\x90").unwrap_err();
631        #[rustfmt::skip]
632        assert_eq!(e.to_string(), "InvalidMsgpack: expected a map: the type decoded isn't match with the expected one");
633
634        let e = FfiSafeContext::decode_msgpack_impl("path", b"\x80").unwrap_err();
635        #[rustfmt::skip]
636        assert_eq!(e.to_string(), "InvalidMsgpack: context must contain a request_id");
637
638        let e = FfiSafeContext::decode_msgpack_impl("path", b"\x87").unwrap_err();
639        #[rustfmt::skip]
640        assert_eq!(e.to_string(), "InvalidMsgpack: not enough entries in mapping: expected 7, got 0");
641
642        let mut data = Vec::new();
643        data.extend(b"\x84");
644        data.extend(b"\x01\xd8\x020123456789abcdef");
645        data.extend(b"\x02\xa6plugin");
646        data.extend(b"\x03\xa7service");
647        data.extend(b"\x04\xa51.2.3");
648        data.extend(b"unexpected-data");
649        let e = FfiSafeContext::decode_msgpack_impl("path", &data).unwrap_err();
650        #[rustfmt::skip]
651        assert_eq!(e.to_string(), r#"InvalidMsgpack: unexpected data after context: b"unexpected-data""#);
652
653        let mut data = Vec::new();
654        data.extend(b"\x85");
655        data.extend(b"\x01\xd8\x02");
656        let e = FfiSafeContext::decode_msgpack_impl("path", &data).unwrap_err();
657        #[rustfmt::skip]
658        assert_eq!(e.to_string(), "InvalidMsgpack: failed decoding field RequestId: IO error while reading data: unexpected end of file");
659
660        let mut data = Vec::new();
661        data.extend(b"\x85");
662        data.extend(b"\x01\xd8\x020123456789abcdef");
663        data.extend(b"\x02\xa6plugin");
664        data.extend(b"\x03\xa7service");
665        data.extend(b"\x04\xa51.2.3");
666        data.extend(b"\xa3foo\xa4");
667        let e = FfiSafeContext::decode_msgpack_impl("path", &data).unwrap_err();
668        #[rustfmt::skip]
669        assert_eq!(e.to_string(), "InvalidMsgpack: msgpack read error: failed to read MessagePack data");
670
671        let mut data = Vec::new();
672        data.extend(b"\x85");
673        data.extend(b"\x01\xd8\x020123456789abcdef");
674        data.extend(b"\x02\xa6plugin");
675        data.extend(b"\x03\xa7service");
676        data.extend(b"\x04\xa51.2.3");
677        data.extend(b"\xa3foo\x80");
678        let context = FfiSafeContext::decode_msgpack_impl("path", &data).unwrap();
679        let context = Context::new(&context);
680        let e = context.get("foo").unwrap_err();
681        #[rustfmt::skip]
682        assert_eq!(e.to_string(), r#"InvalidMsgpack: failed decoding field "foo": data did not match any variant of untagged enum ContextValue"#);
683
684        //
685        // Check sucess case
686        //
687        let mut data = Vec::new();
688        data.extend(b"\x89");
689        data.extend(b"\x01\xd8\x020123456789abcdef");
690        data.extend(b"\x04\xa51.2.3");
691        data.extend(b"\x03\xa7service");
692        data.extend(b"\x02\xa6plugin");
693        data.extend(b"\xa3foo\xa3bar");
694        data.extend(b"\xa4bool\xc3");
695        data.extend(b"\xa3int\x45");
696        data.extend(b"\xa5float\xcb\x40\x09\x1e\xb8\x51\xeb\x85\x1f");
697        data.extend(b"\xa5array\x93\x01\xa3two\x03");
698
699        let context = FfiSafeContext::decode_msgpack_impl("path", &data).unwrap();
700        let context = Context::new(&context);
701        assert_eq!(
702            context.request_id,
703            Uuid::try_from_slice(b"0123456789abcdef").unwrap()
704        );
705        assert_eq!(context.path, "path");
706        assert_eq!(context.plugin_name, "plugin");
707        assert_eq!(context.service_name, "service");
708        assert_eq!(context.plugin_version, "1.2.3");
709        assert!(context.named_fields.get().is_none());
710
711        assert_eq!(
712            *context.get("foo").unwrap().unwrap(),
713            ContextValue::from("bar")
714        );
715        assert_eq!(
716            *context.get("bool").unwrap().unwrap(),
717            ContextValue::from(true)
718        );
719        assert_eq!(
720            *context.get("int").unwrap().unwrap(),
721            ContextValue::from(69)
722        );
723        assert_eq!(
724            *context.get("float").unwrap().unwrap(),
725            ContextValue::from(3.14)
726        );
727        assert_eq!(
728            *context.get("array").unwrap().unwrap(),
729            ContextValue::Array(vec![
730                ContextValue::from(1),
731                ContextValue::from("two"),
732                ContextValue::from(3)
733            ])
734        );
735        assert!(context.get("bar").unwrap().is_none());
736    }
737
738    #[tarantool::test]
739    fn context_get_set() {
740        let mut data = Vec::new();
741        data.extend(b"\x84");
742        data.extend(b"\x01\xd8\x020123456789abcdef");
743        data.extend(b"\x04\xa51.2.3");
744        data.extend(b"\x03\xa7service");
745        data.extend(b"\x02\xa6plugin");
746        let context = FfiSafeContext::decode_msgpack_impl("path", &data).unwrap();
747        let mut context = Context::new(&context);
748
749        assert!(context.get("bar").unwrap().is_none());
750
751        let old_value = context.set("bar", "string").unwrap();
752        assert!(old_value.is_none());
753        assert_eq!(
754            *context.get("bar").unwrap().unwrap(),
755            ContextValue::from("string")
756        );
757
758        let old_value = context.set("bar", 0xba5).unwrap().unwrap();
759        assert_eq!(old_value, ContextValue::from("string"));
760        assert_eq!(
761            *context.get("bar").unwrap().unwrap(),
762            ContextValue::from(0xba5)
763        );
764
765        assert!(!old_value.is_owned());
766        assert!(old_value.into_owned().is_owned());
767    }
768}