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