Skip to main content

powhttp_sdk/
inspector.rs

1use std::sync::Arc;
2use serde::{Serialize, Serializer};
3
4use crate::runtime::handle::ExtensionHandle;
5use crate::runtime::handlers::SingleEntryContext;
6use crate::runtime::state::{MessageTabContentHandler, MessageTabHandlers, MessageTabVisibilityHandler};
7use crate::error::Error;
8
9/// Syntax highlighting applied to the text of a [`TabContent`].
10#[derive(Serialize, Clone, Copy, Debug, PartialEq, Eq)]
11#[serde(rename_all = "lowercase")]
12pub enum Language {
13    Text,
14    Json,
15    Xml,
16    Html,
17    Css,
18    JavaScript,
19    Yaml,
20}
21
22/// The contents rendered inside a [`MessageTab`].
23#[derive(Serialize, Clone, Debug)]
24#[serde(rename_all = "camelCase")]
25pub struct TabContent {
26    text: String,
27    language: Language,
28}
29
30impl TabContent {
31    /// Creates contents with an explicit `language`.
32    pub fn new(text: impl Into<String>, language: Language) -> Self {
33        Self {
34            text: text.into(),
35            language,
36        }
37    }
38
39    /// Creates contents without syntax highlighting.
40    pub fn plain(text: impl Into<String>) -> Self {
41        Self::new(text, Language::Text)
42    }
43
44    /// Creates contents highlighted as JSON.
45    pub fn json(text: impl Into<String>) -> Self {
46        Self::new(text, Language::Json)
47    }
48
49    /// Creates contents highlighted as XML.
50    pub fn xml(text: impl Into<String>) -> Self {
51        Self::new(text, Language::Xml)
52    }
53
54    /// Creates contents highlighted as HTML.
55    pub fn html(text: impl Into<String>) -> Self {
56        Self::new(text, Language::Html)
57    }
58
59    /// Creates contents highlighted as CSS.
60    pub fn css(text: impl Into<String>) -> Self {
61        Self::new(text, Language::Css)
62    }
63
64    /// Creates contents highlighted as JavaScript.
65    pub fn javascript(text: impl Into<String>) -> Self {
66        Self::new(text, Language::JavaScript)
67    }
68
69    /// Creates contents highlighted as YAML.
70    pub fn yaml(text: impl Into<String>) -> Self {
71        Self::new(text, Language::Yaml)
72    }
73}
74
75/// A tab displayed next to Headers, Cookies, Query, Body and Raw in the request
76/// or response section of the Inspector.
77///
78/// The content handler receives a [`SingleEntryContext`] and returns the
79/// [`TabContent`] to render. A tab is visible for every entry unless
80/// [`visible_when`](MessageTab::visible_when) narrows it down.
81///
82/// ```
83/// use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
84/// use powhttp_sdk::{MessageTab, TabContent, ExtensionHandle, SingleEntryContext};
85/// use powhttp_sdk::sessions::SessionEntry;
86///
87/// fn bearer_token(entry: &SessionEntry) -> Option<&str> {
88///     entry.request.headers.get("authorization")?.strip_prefix("Bearer ")
89/// }
90///
91/// fn decode_jwt_payload(token: &str) -> Option<String> {
92///     let payload = token.split('.').nth(1)?;
93///     let bytes = BASE64_URL_SAFE_NO_PAD.decode(payload).ok()?;
94///     String::from_utf8(bytes).ok()
95/// }
96///
97/// let tab = MessageTab::new(
98///     "jwt",
99///     "JWT",
100///     async |ctx: SingleEntryContext, handle: ExtensionHandle| {
101///         let entry = handle.get_session_entry(ctx.session_id, ctx.entry_id).await?;
102///         let payload = entry
103///             .as_ref()
104///             .and_then(bearer_token)
105///             .and_then(decode_jwt_payload)
106///             .unwrap_or_default();
107///         Ok(TabContent::json(payload))
108///     },
109/// )
110/// .visible_when(async |ctx: SingleEntryContext, handle: ExtensionHandle| {
111///     let entry = handle.get_session_entry(ctx.session_id, ctx.entry_id).await?;
112///     Ok(entry.as_ref().and_then(bearer_token).is_some())
113/// });
114/// ```
115#[derive(Serialize)]
116#[serde(rename_all = "camelCase")]
117pub struct MessageTab {
118    id: String,
119    label: String,
120    #[serde(skip)]
121    content: MessageTabContentHandler,
122    #[serde(rename = "isConditional", serialize_with = "serialize_is_some")]
123    visibility: Option<MessageTabVisibilityHandler>,
124}
125
126impl MessageTab {
127    /// Creates a new tab with the given `id`, display `label` and async content handler.
128    pub fn new<F, Fut>(id: impl Into<String>, label: impl Into<String>, content: F) -> Self
129    where
130        F: Fn(SingleEntryContext, ExtensionHandle) -> Fut + Send + Sync + 'static,
131        Fut: Future<Output = Result<TabContent, Error>> + Send + 'static,
132    {
133        Self {
134            id: id.into(),
135            label: label.into(),
136            content: Arc::new(move |ctx, handle| {
137                let fut = content(ctx, handle);
138                Box::pin(async move { fut.await.map_err(Error::into_jrpc) })
139            }),
140            visibility: None,
141        }
142    }
143
144    /// Restricts the tab to entries for which `visibility` resolves to `true`.
145    ///
146    /// The predicate runs whenever an entry is selected, so it should avoid
147    /// expensive work such as fetching bodies.
148    pub fn visible_when<F, Fut>(mut self, visibility: F) -> Self
149    where
150        F: Fn(SingleEntryContext, ExtensionHandle) -> Fut + Send + Sync + 'static,
151        Fut: Future<Output = Result<bool, Error>> + Send + 'static,
152    {
153        self.visibility = Some(Arc::new(move |ctx, handle| {
154            let fut = visibility(ctx, handle);
155            Box::pin(async move { fut.await.map_err(Error::into_jrpc) })
156        }));
157        self
158    }
159
160    pub(crate) fn extract_handlers(&self) -> (String, MessageTabHandlers) {
161        (
162            self.id.clone(),
163            MessageTabHandlers {
164                content: Arc::clone(&self.content),
165                visibility: self.visibility.as_ref().map(Arc::clone),
166            },
167        )
168    }
169}
170
171fn serialize_is_some<S: Serializer>(
172    visibility: &Option<MessageTabVisibilityHandler>,
173    serializer: S,
174) -> Result<S::Ok, S::Error> {
175    serializer.serialize_bool(visibility.is_some())
176}