picodata_plugin/transport/
context.rs

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