diff --git a/dev_plugin/src/detail-view.tsx b/dev_plugin/src/detail-view.tsx index 075d0a9..3040b48 100644 --- a/dev_plugin/src/detail-view.tsx +++ b/dev_plugin/src/detail-view.tsx @@ -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 { H4 Title H5 Title H6 Title - + Code block Test diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index 4f0f889..486b8b2 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -42,7 +42,7 @@ declare global { children?: ElementComponent; }; ["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> = Element | EmptyNode | Iterable>; export type StringComponent = StringNode | EmptyNode | Iterable; export type StringOrElementComponent> = StringNode | EmptyNode | Element | Iterable>; +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 = (props: ImageProps): ReactNode => { return ; @@ -623,7 +623,7 @@ Inline.Center = Content; export interface EmptyViewProps { title: string; description?: string; - image?: ImageSource; + image?: ImageSource | Icons; } export const EmptyView: FC = (props: EmptyViewProps): ReactNode => { return ; diff --git a/js/api_build/src/index.ts b/js/api_build/src/index.ts index 03e7648..3742f9e 100644 --- a/js/api_build/src/index.ts +++ b/js/api_build/src/index.ts @@ -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 ) } diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index a41b0ca..269bbf8 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -116,49 +116,9 @@ export function getEntrypointPreferences(): Record { } 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 = { diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index b598b67..cc3b9c9 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -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" diff --git a/rust/client/build.rs b/rust/client/build.rs index 7caa4df..9412dfb 100644 --- a/rust/client/build.rs +++ b/rust/client/build.rs @@ -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".to_owned(), + PropertyType::ImageSource => "Vec".to_owned(), PropertyType::Union { .. } => union_name, PropertyType::Object { name } => name.to_owned(), PropertyType::Enum { name } => name.to_owned() diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 70f228c..be44b70 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -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> = 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> = 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 { 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) -> anyhow::Result { - Ok(ImageSource { - data: parse_bytes(value, "data")?, - }) +impl UiPropertyValueToEnum for EmptyViewImage { + fn convert(value: &UiPropertyValue) -> anyhow::Result { + 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")) + } } -} \ No newline at end of file +} + +impl UiPropertyValueToEnum for ImageSource { + fn convert(value: &UiPropertyValue) -> anyhow::Result { + 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")) + } + } +} diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index e6e7ead..50ee102 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -82,8 +82,8 @@ pub enum PropertyType { Function { arguments: Vec }, - #[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 { 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 { [ 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 { 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(), diff --git a/rust/server/src/plugins/js/ui.rs b/rust/server/src/plugins/js/ui.rs index d0597c4..6fe5117 100644 --- a/rust/server/src/plugins/js/ui.rs +++ b/rust/server/src/plugins/js/ui.rs @@ -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) -> anyhow::Result { +fn from_js_to_intermediate_widget(state: Rc>, scope: &mut v8::HandleScope, ui_widget: JsUiWidget, component_model: &ComponentModel, shared_types: &IndexMap) -> anyhow::Result { 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::>>()?; 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::>(); - 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>, scope: &mut v8::HandleScope, v8_properties: HashMap, 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::>>()?; @@ -201,6 +204,7 @@ fn from_js_to_intermediate_properties( } fn convert( + state: Rc>, 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) -> 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, object_name: &str, shared_types: &IndexMap) -> anyhow::Result<(String, UiPropertyValue)> { +fn convert_object( + state: Rc>, + scope: &mut v8::HandleScope, + name: String, + value: v8::Local, + object_name: &str, + shared_types: &IndexMap +) -> anyhow::Result<(String, UiPropertyValue)> { let object: v8::Local = 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 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>, name: String, source: ImageSource) -> anyhow::Result<(String, UiPropertyValue)> { + match source { + ImageSource::Asset { asset } => { + let bytes = { + let state = state.borrow(); + + let plugin_id = state + .borrow::() + .plugin_id() + .clone(); + + let repository = state + .borrow::() + .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 +) -> 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 = 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 = 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 = 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 + // } +} \ No newline at end of file