ImageSource on js plugin side is now just a reference to asset instead of binary array with image

This commit is contained in:
Exidex 2024-07-21 13:30:26 +02:00
parent af0b0ca51b
commit 5c19f79574
9 changed files with 210 additions and 120 deletions

View file

@ -60,8 +60,6 @@ export default function DetailView(): ReactElement {
const env = Deno.env.get("RUST_LOG");
console.log("RUST_LOG:", env);
const logoData = assetData("logo.png");
console.error("DetailView error")
useEffect(() => {
@ -113,7 +111,7 @@ export default function DetailView(): ReactElement {
<Detail.Content.H4>H4 Title</Detail.Content.H4>
<Detail.Content.H5>H5 Title</Detail.Content.H5>
<Detail.Content.H6>H6 Title</Detail.Content.H6>
<Detail.Content.Image source={{ data: logoData }}/>
<Detail.Content.Image source={{ asset: "logo.png" }}/>
<Detail.Content.CodeBlock>Code block Test</Detail.Content.CodeBlock>
<Detail.Content.HorizontalBreak/>
<Detail.Content.Paragraph>

View file

@ -42,7 +42,7 @@ declare global {
children?: ElementComponent<typeof MetadataTagList | typeof MetadataLink | typeof MetadataValue | typeof MetadataIcon | typeof MetadataSeparator>;
};
["gauntlet:image"]: {
source: ImageSource;
source: ImageSource | Icons;
};
["gauntlet:h1"]: {
children?: StringComponent;
@ -119,7 +119,7 @@ declare global {
["gauntlet:empty_view"]: {
title: string;
description?: string;
image?: ImageSource;
image?: ImageSource | Icons;
};
["gauntlet:list_item"]: {
id: string;
@ -163,6 +163,9 @@ export type EmptyNode = boolean | null | undefined;
export type ElementComponent<Comp extends FC<any>> = Element<Comp> | EmptyNode | Iterable<ElementComponent<Comp>>;
export type StringComponent = StringNode | EmptyNode | Iterable<StringComponent>;
export type StringOrElementComponent<Comp extends FC<any>> = StringNode | EmptyNode | Element<Comp> | Iterable<StringOrElementComponent<Comp>>;
export type ImageSource = {
asset: string;
};
export enum Icons {
PersonAdd = "PersonAdd",
Airplane = "Airplane",
@ -336,9 +339,6 @@ export enum Icons {
Indent = "Indent",
Unindent = "Unindent"
}
export type ImageSource = {
data: ArrayBuffer;
};
export interface ActionProps {
id?: string;
title: string;
@ -429,7 +429,7 @@ Metadata.Value = MetadataValue;
Metadata.Icon = MetadataIcon;
Metadata.Separator = MetadataSeparator;
export interface ImageProps {
source: ImageSource;
source: ImageSource | Icons;
}
export const Image: FC<ImageProps> = (props: ImageProps): ReactNode => {
return <gauntlet:image source={props.source}></gauntlet:image>;
@ -623,7 +623,7 @@ Inline.Center = Content;
export interface EmptyViewProps {
title: string;
description?: string;
image?: ImageSource;
image?: ImageSource | Icons;
}
export const EmptyView: FC<EmptyViewProps> = (props: EmptyViewProps): ReactNode => {
return <gauntlet:empty_view title={props.title} description={props.description} image={props.image}></gauntlet:empty_view>;

View file

@ -241,6 +241,31 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
const root = modelInput.find((component): component is RootComponent => component.type === "root");
if (root != null) {
// image special case
// export type ImageSource = { asset: string } | { url: string };
const imageSourceDeclaration = ts.factory.createTypeAliasDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier("ImageSource"),
undefined,
ts.factory.createUnionTypeNode([
ts.factory.createTypeLiteralNode([ts.factory.createPropertySignature(
undefined,
ts.factory.createIdentifier("asset"),
undefined,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
)]),
// ts.factory.createTypeLiteralNode([ts.factory.createPropertySignature( // TODO implement url imagesource
// undefined,
// ts.factory.createIdentifier("url"),
// undefined,
// ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
// )])
])
);
publicDeclarations.push(imageSourceDeclaration)
for (const [name, sharedType] of Object.entries(root.sharedTypes)) {
switch (sharedType.type) {
@ -633,9 +658,9 @@ function makeType(type: PropertyType): ts.TypeNode {
]
)
}
case "image_data": {
case "image_source": {
return ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("ArrayBuffer"),
ts.factory.createIdentifier("ImageSource"),
undefined
)
}

View file

@ -116,49 +116,9 @@ export function getEntrypointPreferences(): Record<string, any> {
}
function createWidget(hostContext: HostContext, type: ComponentType, properties: Props, children: UiWidget[] = []): Instance {
const component = hostContext.componentModel[type];
const rootComponent = hostContext.componentModel["gauntlet:root"] as RootComponent;
const sharedTypes = rootComponent.sharedTypes;
const props = Object.fromEntries(
Object.entries(properties)
.filter(([key, _]) => key !== "children")
.map(([key, value]) => {
if (component.type === "standard" && !!value) {
const prop = component.props.find(prop => prop.name === key)
if (prop) {
switch (prop.type.type) {
case "image_data": {
return [key, Array.from(new Uint8Array(value))]
}
case "object": {
// TODO nested objects?
const sharedType = sharedTypes[prop.type.name]!!;
if (sharedType.type !== "object" || typeof value !== "object") {
throw new Error(key + " property is expected to be an object")
}
const object = Object.fromEntries(
Object.entries(value)
.map(([key, value]) => {
const prop = sharedType.items[key]
if (prop.type === "image_data") {
return [key, Array.from(new Uint8Array(value as any))] // TODO arraybuffer? fix when migrating to deno's op2
}
return [key, value]
})
);
return [key, object]
}
}
}
}
return [key, value]
})
);
const instance: Instance = {

View file

@ -182,7 +182,7 @@ type ComponentRef = {
componentName: string,
}
type PropertyType = TypeString | TypeNumber | TypeBoolean | TypeComponent | TypeFunction | TypeImageData | TypeImageEnum | TypeImageUnion | TypeImageObject
type PropertyType = TypeString | TypeNumber | TypeBoolean | TypeComponent | TypeFunction | TypeImageSource | TypeImageEnum | TypeImageUnion | TypeImageObject
type TypeString = {
type: "string"
@ -201,8 +201,8 @@ type TypeFunction = {
type: "function"
arguments: Property[]
}
type TypeImageData = {
type: "image_data"
type TypeImageSource = {
type: "image_source"
}
type TypeImageEnum = {
type: "enum"

View file

@ -177,7 +177,7 @@ fn main() -> anyhow::Result<()> {
PropertyType::Component { .. } => {
// component properties are found in a children array
}
PropertyType::ImageData => {
PropertyType::ImageSource => {
if prop.optional {
output.push_str(&format!(" {}: parse_bytes_optional(&properties, \"{}\")?,\n", prop.name, prop.name));
} else {
@ -520,7 +520,7 @@ fn generate_required_type(property_type: &PropertyType, union_name: String) -> S
PropertyType::Boolean => "bool".to_owned(),
PropertyType::Function { .. } => panic!("client doesn't know about functions in properties"),
PropertyType::Component { .. } => panic!("component properties are found in children array"),
PropertyType::ImageData => "Vec<u8>".to_owned(),
PropertyType::ImageSource => "Vec<u8>".to_owned(),
PropertyType::Union { .. } => union_name,
PropertyType::Object { name } => name.to_owned(),
PropertyType::Enum { name } => name.to_owned()

View file

@ -4,7 +4,7 @@ use std::str::FromStr;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use anyhow::anyhow;
use iced::{Alignment, Application, Font, Length};
use iced::{Alignment, Font, Length};
use iced::alignment::Horizontal;
use iced::font::Weight;
use iced::widget::{button, checkbox, column, container, horizontal_rule, horizontal_space, image, pick_list, row, scrollable, Space, text, text_input, tooltip, vertical_rule};
@ -435,8 +435,17 @@ impl ComponentWidgetWrapper {
ComponentWidget::Image { source } => {
let centered = context.is_content_centered();
let content: Element<_> = image(Handle::from_memory(source.data.clone())) // FIXME really expensive clone
.into();
let content: Element<_> = match source {
ImageSource::_0(bytes) => {
image(Handle::from_memory(bytes.clone())) // FIXME really expensive clone
.into()
}
ImageSource::_1(icon) => {
text(icon_to_bootstrap(icon))
.font(icons::BOOTSTRAP_FONT) // TODO size, height and width
.into()
}
};
let mut content = container(content)
.width(Length::Fill);
@ -828,11 +837,20 @@ impl ComponentWidgetWrapper {
content
}
ComponentWidget::EmptyView { title, description, image } => {
let image: Option<Element<_>> = image.as_ref()
.map(|image| {
iced::widget::image(Handle::from_memory(image.data.clone())) // FIXME really expensive clone
.themed(ImageStyle::EmptyViewImage)
ComponentWidget::EmptyView { title, description, image: empty_view_image } => {
let image: Option<Element<_>> = empty_view_image.as_ref()
.map(|empty_view_image| {
match empty_view_image {
EmptyViewImage::_0(bytes) => {
image(Handle::from_memory(bytes.clone())) // FIXME really expensive clone
.themed(ImageStyle::EmptyViewImage)
}
EmptyViewImage::_1(icon) => {
text(icon_to_bootstrap(icon))
.font(icons::BOOTSTRAP_FONT) // TODO size, height and width
.into()
}
}
});
let title: Element<_> = text(title)
@ -875,7 +893,7 @@ impl ComponentWidgetWrapper {
.map(|icon| {
match icon {
ListItemIcon::_0(bytes) => {
image(Handle::from_memory(bytes.data.clone())) // FIXME really expensive clone
image(Handle::from_memory(bytes.clone())) // FIXME really expensive clone
.into()
},
ListItemIcon::_1(icon) => {
@ -1916,23 +1934,37 @@ impl UiPropertyValueToEnum for ListItemIcon {
fn convert(value: &UiPropertyValue) -> anyhow::Result<ListItemIcon> {
match value {
UiPropertyValue::String(value) => Ok(ListItemIcon::_1(Icons::from_str(value)?)),
UiPropertyValue::Bytes(value) => Ok(ListItemIcon::_0(value.clone())), // FIXME really expensive clone
UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number for ListItemIcon")),
UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool for ListItemIcon")),
UiPropertyValue::Bytes(_) => Err(anyhow!("unexpected type bytes for ListItemIcon")),
UiPropertyValue::Object(value) => {
Ok(ListItemIcon::_0(ImageSource {
data: parse_bytes(value, "data")?,
}))
}
UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object for ListItemIcon")),
UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined for ListItemIcon"))
}
}
}
impl UiPropertyValueToStruct for ImageSource {
fn convert(value: &HashMap<String, UiPropertyValue>) -> anyhow::Result<ImageSource> {
Ok(ImageSource {
data: parse_bytes(value, "data")?,
})
impl UiPropertyValueToEnum for EmptyViewImage {
fn convert(value: &UiPropertyValue) -> anyhow::Result<EmptyViewImage> {
match value {
UiPropertyValue::String(value) => Ok(EmptyViewImage::_1(Icons::from_str(value)?)),
UiPropertyValue::Bytes(value) => Ok(EmptyViewImage::_0(value.clone())), // FIXME really expensive clone
UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number for ListItemIcon")),
UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool for ListItemIcon")),
UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object for ListItemIcon")),
UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined for ListItemIcon"))
}
}
}
}
impl UiPropertyValueToEnum for ImageSource {
fn convert(value: &UiPropertyValue) -> anyhow::Result<ImageSource> {
match value {
UiPropertyValue::String(value) => Ok(ImageSource::_1(Icons::from_str(value)?)),
UiPropertyValue::Bytes(value) => Ok(ImageSource::_0(value.clone())), // FIXME really expensive clone
UiPropertyValue::Number(_) => Err(anyhow!("unexpected type number for ListItemIcon")),
UiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool for ListItemIcon")),
UiPropertyValue::Object(_) => Err(anyhow!("unexpected type object for ListItemIcon")),
UiPropertyValue::Undefined => Err(anyhow!("unexpected type undefined for ListItemIcon"))
}
}
}

View file

@ -82,8 +82,8 @@ pub enum PropertyType {
Function {
arguments: Vec<Property>
},
#[serde(rename = "image_data")]
ImageData,
#[serde(rename = "image_source")]
ImageSource,
#[serde(rename = "enum")]
Enum {
name: String
@ -477,10 +477,6 @@ fn root(children: &[&Component]) -> Component {
].into_iter().map(|s| s.to_string()).collect()
}),
(
"ImageSource".to_owned(),
SharedType::Object { items: IndexMap::from([("data".to_owned(), PropertyType::ImageData)]) }
)
]),
}
}
@ -639,7 +635,7 @@ pub fn create_component_model() -> Vec<Component> {
mark_doc!("/image/description.md"),
"Image",
[
property("source", mark_doc!("/image/props/source.md"), false, PropertyType::Object { name: "ImageSource".to_owned() })
property("source", mark_doc!("/image/props/source.md"), false, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] })
],
children_none(),
);
@ -917,7 +913,7 @@ pub fn create_component_model() -> Vec<Component> {
[
property("title", mark_doc!("/empty_view/props/title.md"),false, PropertyType::String),
property("description", mark_doc!("/empty_view/props/description.md"),true, PropertyType::String),
property("image", mark_doc!("/empty_view/props/image.md"),true, PropertyType::Object { name: "ImageSource".to_owned() }),
property("image", mark_doc!("/empty_view/props/image.md"),true, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }),
],
children_none(),
);
@ -930,7 +926,7 @@ pub fn create_component_model() -> Vec<Component> {
property("id", mark_doc!("/list_item/props/id.md"),false, PropertyType::String),
property("title", mark_doc!("/list_item/props/title.md"),false, PropertyType::String),
property("subtitle", mark_doc!("/list_item/props/subtitle.md"),true, PropertyType::String),
property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::Union { items: vec![PropertyType::Object { name: "ImageSource".to_owned() }, PropertyType::Enum { name: "Icons".to_owned() }] }),
property("icon", mark_doc!("/list_item/props/icon.md"),true, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }),
// accessories
],
children_none(),

View file

@ -3,8 +3,10 @@ use std::collections::HashMap;
use std::rc::Rc;
use anyhow::{anyhow, Context};
use deno_core::{op, OpState, serde_v8, v8};
use deno_core::futures::executor::block_on;
use deno_core::v8::{GetPropertyNamesArgs, KeyConversionMode, PropertyFilter};
use indexmap::IndexMap;
use serde::Deserialize;
use common::model::{EntrypointId, PhysicalKey, UiPropertyValue, UiWidget};
use component_model::{Component, Property, PropertyType, SharedType};
use crate::model::{JsUiRenderLocation, JsUiRequestData, JsUiResponseData, JsUiWidget};
@ -88,7 +90,7 @@ fn op_react_replace_view(
entrypoint_id: EntrypointId::from_string(entrypoint_id),
render_location,
top_level_view,
container: from_js_to_intermediate_widget(scope, container, component_model, shared_types)?,
container: from_js_to_intermediate_widget(state.clone(), scope, container, component_model, shared_types)?,
};
match make_request(&state, data).context("ReplaceView frontend response")? {
@ -146,9 +148,9 @@ async fn fetch_action_id_for_shortcut(
Ok(result)
}
fn from_js_to_intermediate_widget(scope: &mut v8::HandleScope, ui_widget: JsUiWidget, component_model: &ComponentModel, shared_types: &IndexMap<String, SharedType>) -> anyhow::Result<UiWidget> {
fn from_js_to_intermediate_widget(state: Rc<RefCell<OpState>>, scope: &mut v8::HandleScope, ui_widget: JsUiWidget, component_model: &ComponentModel, shared_types: &IndexMap<String, SharedType>) -> anyhow::Result<UiWidget> {
let children = ui_widget.widget_children.into_iter()
.map(|child| from_js_to_intermediate_widget(scope, child, component_model, shared_types))
.map(|child| from_js_to_intermediate_widget(state.clone(), scope, child, component_model, shared_types))
.collect::<anyhow::Result<Vec<UiWidget>>>()?;
let component = component_model.components
@ -167,7 +169,7 @@ fn from_js_to_intermediate_widget(scope: &mut v8::HandleScope, ui_widget: JsUiWi
.map(|prop| (&prop.name, &prop.property_type))
.collect::<HashMap<_, _>>();
let properties = from_js_to_intermediate_properties(scope, ui_widget.widget_properties, &props, shared_types);
let properties = from_js_to_intermediate_properties(state.clone(), scope, ui_widget.widget_properties, &props, shared_types);
Ok(UiWidget {
widget_id: ui_widget.widget_id,
@ -178,6 +180,7 @@ fn from_js_to_intermediate_widget(scope: &mut v8::HandleScope, ui_widget: JsUiWi
}
fn from_js_to_intermediate_properties(
state: Rc<RefCell<OpState>>,
scope: &mut v8::HandleScope,
v8_properties: HashMap<String, serde_v8::Value>,
component_props: &HashMap<&String, &PropertyType>,
@ -193,7 +196,7 @@ fn from_js_to_intermediate_properties(
return Err(anyhow!("unknown property encountered {:?}", name))
};
convert(scope, property_type, name, val, shared_types)
convert(state.clone(), scope, property_type, name, val, shared_types)
})
.collect::<anyhow::Result<Vec<(_, _)>>>()?;
@ -201,6 +204,7 @@ fn from_js_to_intermediate_properties(
}
fn convert(
state: Rc<RefCell<OpState>>,
scope: &mut v8::HandleScope,
property_type: &PropertyType,
name: String,
@ -235,16 +239,13 @@ fn convert(
PropertyType::Function { .. } => {
panic!("functions are filtered out")
}
PropertyType::ImageData => {
if value.is_array() { // TODO arraybuffer? fix when migrating to deno's op2
convert_bytes(scope, name, value)
} else {
invalid_type_err(name, value, property_type)
}
PropertyType::ImageSource => {
let source: ImageSource = serde_v8::from_v8(scope, value)?;
convert_image_source(state.clone(), name, source)
}
PropertyType::Object { name: object_name } => {
if value.is_object() {
convert_object(scope, name, value, object_name, shared_types)
convert_object(state.clone(), scope, name, value, object_name, shared_types)
} else {
invalid_type_err(name, value, property_type)
}
@ -265,21 +266,34 @@ fn convert(
None => invalid_type_err(name, value, property_type),
Some(_) => convert_boolean(scope, name, value)
}
} else if value.is_array() { // TODO arraybuffer? fix when migrating to deno's op2
match items.iter().find(|prop_type| matches!(prop_type, PropertyType::ImageData)) {
None => invalid_type_err(name, value, property_type),
Some(_) => convert_bytes(scope, name, value)
}
} else if value.is_object() {
match items.iter().find(|prop_type| matches!(prop_type, PropertyType::Object { .. })) {
None => invalid_type_err(name, value, property_type),
Some(PropertyType::Object { name: object_name }) => {
convert_object(scope, name, value, object_name, shared_types)
},
_ => unreachable!()
}
} else {
invalid_type_err(name, value, property_type)
if !value.is_object() {
invalid_type_err(name, value, property_type)
} else {
let image_source = items.iter().find(|prop_type| matches!(prop_type, PropertyType::ImageSource));
let object = items.iter().find(|prop_type| matches!(prop_type, PropertyType::Object { .. }));
match (image_source, object) {
(Some(PropertyType::ImageSource), Some(PropertyType::Object { .. })) => {
panic!("image_source and object is conflicting prop_types")
}
(None, Some(PropertyType::Object { name: object_name })) => {
convert_object(state.clone(), scope, name, value, &object_name, shared_types)
}
(Some(PropertyType::ImageSource), None) => {
println!("test: {}", debug_object_to_json(scope, value.clone()));
let source: ImageSource = serde_v8::from_v8(scope, value)?;
convert_image_source(state.clone(), name, source)
}
(Some(_), Some(_)) | (Some(_), None) | (None, Some(_)) => {
unreachable!()
}
(None, None) => {
invalid_type_err(name, value, property_type)
}
}
}
}
}
}
@ -297,11 +311,14 @@ fn convert_boolean(scope: &mut v8::HandleScope, name: String, value: v8::Local<v
Ok((name, UiPropertyValue::Bool(value.boolean_value(scope))))
}
fn convert_bytes(scope: &mut v8::HandleScope, name: String, value: v8::Local<v8::Value>) -> anyhow::Result<(String, UiPropertyValue)> {
Ok((name, UiPropertyValue::Bytes(serde_v8::from_v8(scope, value)?)))
}
fn convert_object(scope: &mut v8::HandleScope, name: String, value: v8::Local<v8::Value>, object_name: &str, shared_types: &IndexMap<String, SharedType>) -> anyhow::Result<(String, UiPropertyValue)> {
fn convert_object(
state: Rc<RefCell<OpState>>,
scope: &mut v8::HandleScope,
name: String,
value: v8::Local<v8::Value>,
object_name: &str,
shared_types: &IndexMap<String, SharedType>
) -> anyhow::Result<(String, UiPropertyValue)> {
let object: v8::Local<v8::Object> = value.try_into().context(format!("error while reading property {}", name))?;
let props = object
@ -324,7 +341,7 @@ fn convert_object(scope: &mut v8::HandleScope, name: String, value: v8::Local<v8
SharedType::Object { items } => items.get(&key).unwrap()
};
let (key, value) = convert(scope, property_type, key, value, shared_types)?;
let (key, value) = convert(state.clone(), scope, property_type, key, value, shared_types)?;
result_obj.insert(key, value);
}
@ -347,7 +364,7 @@ fn expected_type(prop_type: &PropertyType) -> String {
PropertyType::Function { .. } => {
panic!("functions are filtered out")
}
PropertyType::ImageData => "bytearray".to_owned(),
PropertyType::ImageSource => "ImageSource".to_owned(),
PropertyType::Enum { .. } => "enum".to_owned(),
PropertyType::Union { items } => {
items.into_iter()
@ -358,3 +375,65 @@ fn expected_type(prop_type: &PropertyType) -> String {
PropertyType::Object { .. } => "object".to_owned(),
}
}
fn convert_image_source(state: Rc<RefCell<OpState>>, name: String, source: ImageSource) -> anyhow::Result<(String, UiPropertyValue)> {
match source {
ImageSource::Asset { asset } => {
let bytes = {
let state = state.borrow();
let plugin_id = state
.borrow::<PluginData>()
.plugin_id()
.clone();
let repository = state
.borrow::<DataDbRepository>()
.clone();
block_on(async {
repository.get_asset_data(&plugin_id.to_string(), &asset).await
})?
};
Ok((name, UiPropertyValue::Bytes(bytes)))
}
// ImageSource::Url { url } => { TODO implement url imagesource
// Ok((name, UiPropertyValue::Bytes()))
// }
}
}
fn debug_object_to_json(
scope: &mut v8::HandleScope,
val: v8::Local<v8::Value>
) -> String {
let local = scope.get_current_context();
let global = local.global(scope);
let json_string = v8::String::new(scope, "Deno").expect("Unable to create Deno string");
let json_object = global.get(scope, json_string.into()).expect("Global Deno object not found");
let json_object: v8::Local<v8::Object> = json_object.try_into().expect("Deno value is not an object");
let inspect_string = v8::String::new(scope, "inspect").expect("Unable to create inspect string");
let inspect_object = json_object.get(scope, inspect_string.into()).expect("Unable to get inspect on global Deno object");
let stringify_fn: v8::Local<v8::Function> = inspect_object.try_into().expect("inspect value is not a function");;
let undefined = v8::undefined(scope).into();
let json_object = stringify_fn.call(scope, undefined, &[val]).expect("Unable to get serialize prop");
let json_string: v8::Local<v8::String> = json_object.try_into().expect("result is not a string");
let result = json_string.to_rust_string_lossy(scope);
result
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ImageSource {
Asset {
asset: String
},
// Url { TODO implement url imagesource
// url: String
// }
}