diff --git a/Cargo.lock b/Cargo.lock index 703d07b..e0cbef6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -850,8 +850,6 @@ dependencies = [ "iced", "iced_aw", "itertools 0.12.1", - "serde", - "serde_json", "strum 0.26.2", "thiserror", "tokio", diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index ed8f08a..c6aa56d 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -42,9 +42,7 @@ declare global { children?: ElementComponent; }; ["gauntlet:image"]: { - source: { - data: ArrayBuffer; - }; + source: ImageSource; }; ["gauntlet:h1"]: { children?: StringComponent; @@ -119,17 +117,13 @@ declare global { ["gauntlet:empty_view"]: { title: string; description?: string; - image?: { - data: ArrayBuffer; - }; + image?: ImageSource; }; ["gauntlet:list_item"]: { id: string; title: string; subtitle?: string; - icon?: { - data: ArrayBuffer; - } | Icons; + icon?: ImageSource | Icons; }; ["gauntlet:list_section"]: { children?: ElementComponent; @@ -340,6 +334,9 @@ export enum Icons { Indent = "Indent", Unindent = "Unindent" } +export type ImageSource = { + data: ArrayBuffer; +}; export interface ActionProps { id?: string; title: string; @@ -430,9 +427,7 @@ Metadata.Value = MetadataValue; Metadata.Icon = MetadataIcon; Metadata.Separator = MetadataSeparator; export interface ImageProps { - source: { - data: ArrayBuffer; - }; + source: ImageSource; } export const Image: FC = (props: ImageProps): ReactNode => { return ; @@ -623,9 +618,7 @@ Inline.Center = Content; export interface EmptyViewProps { title: string; description?: string; - image?: { - data: ArrayBuffer; - }; + image?: ImageSource; } export const EmptyView: FC = (props: EmptyViewProps): ReactNode => { return ; @@ -634,9 +627,7 @@ export interface ListItemProps { id: string; title: string; subtitle?: string; - icon?: { - data: ArrayBuffer; - } | Icons; + icon?: ImageSource | Icons; } export const ListItem: FC = (props: ListItemProps): ReactNode => { return ; diff --git a/js/api_build/src/index.ts b/js/api_build/src/index.ts index 96a787c..5d1f5ec 100644 --- a/js/api_build/src/index.ts +++ b/js/api_build/src/index.ts @@ -639,18 +639,11 @@ function makeType(type: PropertyType): ts.TypeNode { ] ) } - case "image_source": { - return ts.factory.createTypeLiteralNode([ - ts.factory.createPropertySignature( - undefined, - ts.factory.createIdentifier("data"), - undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("ArrayBuffer"), - undefined - ) - ) - ]) + case "image_data": { + return ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("ArrayBuffer"), + undefined + ) } case "enum": { return ts.factory.createTypeReferenceNode( diff --git a/js/react_renderer/src/renderer.ts b/js/react_renderer/src/renderer.ts index 782fd2e..12607ee 100644 --- a/js/react_renderer/src/renderer.ts +++ b/js/react_renderer/src/renderer.ts @@ -111,6 +111,8 @@ 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) @@ -118,11 +120,33 @@ function createWidget(hostContext: HostContext, type: ComponentType, properties: .map(([key, value]) => { if (component.type === "standard" && !!value) { const prop = component.props.find(prop => prop.name === key) - if (prop && prop.type.type === "image_source") { - if (value.data !== undefined) { - return [key, Array.from(new Uint8Array(value.data))] - } else { - throw new Error("'data' property should be provided on image source property") + + 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 ugly cast + } + + return [key, value] + }) + ); + + return [key, object] + } } } } diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index e828e53..d154055 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -173,7 +173,7 @@ type ComponentRef = { componentName: string, } -type PropertyType = TypeString | TypeNumber | TypeBoolean | TypeComponent | TypeFunction | TypeImageSource | TypeImageEnum | TypeImageUnion | TypeImageObject +type PropertyType = TypeString | TypeNumber | TypeBoolean | TypeComponent | TypeFunction | TypeImageData | TypeImageEnum | TypeImageUnion | TypeImageObject type TypeString = { type: "string" @@ -192,8 +192,8 @@ type TypeFunction = { type: "function" arguments: Property[] } -type TypeImageSource = { - type: "image_source" +type TypeImageData = { + type: "image_data" } type TypeImageEnum = { type: "enum" diff --git a/rust/client/Cargo.toml b/rust/client/Cargo.toml index 5d8d0b0..d3d23d7 100644 --- a/rust/client/Cargo.toml +++ b/rust/client/Cargo.toml @@ -18,8 +18,6 @@ tonic = "0.11.0" global-hotkey = "0.4.2" itertools = "0.12.1" component_model = { path = "../component_model" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" strum = { version = "0.26", features = ["derive"] } [build-dependencies] diff --git a/rust/client/build.rs b/rust/client/build.rs index 90082af..81b8174 100644 --- a/rust/client/build.rs +++ b/rust/client/build.rs @@ -79,8 +79,7 @@ fn main() -> anyhow::Result<()> { for prop in props { match &prop.property_type { PropertyType::Union { items } => { - output.push_str("#[derive(Debug, serde::Deserialize)]\n"); - output.push_str("#[serde(untagged)]\n"); + output.push_str("#[derive(Debug)]\n"); output.push_str(&format!("enum {}{} {{\n", component_name, prop.name.to_case(Case::Pascal))); for (index, property_type) in items.iter().enumerate() { @@ -103,7 +102,7 @@ fn main() -> anyhow::Result<()> { for (type_name, shared_type) in shared_types { match shared_type { SharedType::Enum { items } => { - output.push_str("#[derive(Debug, strum::EnumString, serde::Deserialize)]\n"); + output.push_str("#[derive(Debug, strum::EnumString)]\n"); output.push_str(&format!("enum {} {{\n", type_name)); for item in items { @@ -114,11 +113,11 @@ fn main() -> anyhow::Result<()> { output.push_str("\n"); } SharedType::Object { items } => { - output.push_str("#[derive(Debug, serde::Deserialize)]\n"); + output.push_str("#[derive(Debug)]\n"); output.push_str(&format!("struct {} {{\n", type_name)); for (property_name, property_type) in items { - output.push_str(&format!(" {}: {},\n", &type_name, generate_required_type(&property_type, format!("{}{}", type_name, property_name)))); + output.push_str(&format!(" {}: {},\n", &property_name, generate_required_type(&property_type, format!("{}{}", type_name, property_name)))); } output.push_str("}\n"); @@ -178,7 +177,7 @@ fn main() -> anyhow::Result<()> { PropertyType::Component { .. } => { // component properties are found in a children array } - PropertyType::ImageSource => { + PropertyType::ImageData => { if prop.optional { output.push_str(&format!(" {}: parse_bytes_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); } else { @@ -192,11 +191,18 @@ fn main() -> anyhow::Result<()> { output.push_str(&format!(" {}: parse_enum(&properties, \"{}\")?,\n", prop.name, prop.name)); } } - PropertyType::Union { .. } | PropertyType::Object { .. } => { + PropertyType::Union { .. } => { if prop.optional { - output.push_str(&format!(" {}: parse_json_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); + output.push_str(&format!(" {}: parse_union_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); } else { - output.push_str(&format!(" {}: parse_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); + output.push_str(&format!(" {}: parse_union(&properties, \"{}\")?,\n", prop.name, prop.name)); + } + } + PropertyType::Object { .. } => { + if prop.optional { + output.push_str(&format!(" {}: parse_object_optional(&properties, \"{}\")?,\n", prop.name, prop.name)); + } else { + output.push_str(&format!(" {}: parse_object(&properties, \"{}\")?,\n", prop.name, prop.name)); } } }; @@ -514,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::ImageSource => "Vec".to_owned(), + PropertyType::ImageData => "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/model.rs b/rust/client/src/model.rs index 32f4435..d3cb770 100644 --- a/rust/client/src/model.rs +++ b/rust/client/src/model.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use anyhow::anyhow; -use serde::de::DeserializeOwned; use common::model::{EntrypointId, PluginId, PropertyValue, RenderLocation}; use common::rpc::{RpcUiPropertyValue, RpcUiWidget}; @@ -73,7 +72,7 @@ pub fn from_rpc(value: HashMap) -> anyhow::Result NativeUiPropertyValue::Number(value), Value::Bool(value) => NativeUiPropertyValue::Bool(value), Value::Bytes(value) => NativeUiPropertyValue::Bytes(value), - Value::Json(value) => NativeUiPropertyValue::Json(value), + Value::Object(value) => NativeUiPropertyValue::Object(property_value_from_rpc(value.value)), _ => { return Err(anyhow!("invalid type")) } @@ -88,13 +87,31 @@ pub fn from_rpc(value: HashMap) -> anyhow::Result) -> HashMap { + map.into_iter() + .map(|(name, value)| { + let value = match value.value.unwrap() { + Value::Undefined(_) => unreachable!(), + Value::String(value) => NativeUiPropertyValue::String(value), + Value::Number(value) => NativeUiPropertyValue::Number(value), + Value::Bool(value) => NativeUiPropertyValue::Bool(value), + Value::Bytes(value) => NativeUiPropertyValue::Bytes(value), + Value::Object(value) => NativeUiPropertyValue::Object(property_value_from_rpc(value.value)), + }; + + (name, value) + }) + .collect() +} + #[derive(Debug, Clone)] pub enum NativeUiPropertyValue { String(String), Number(f64), Bool(bool), Bytes(Vec), - Json(String), + Object(HashMap), } impl NativeUiPropertyValue { @@ -126,13 +143,24 @@ impl NativeUiPropertyValue { None } } - pub fn as_json(&self) -> Option { - if let NativeUiPropertyValue::Json(val) = self { - Some(serde_json::from_str(val).expect("invalid json sent from backend?")) + pub fn as_object(&self) -> Option { + if let NativeUiPropertyValue::Object(val) = self { + Some(ValueToStruct::convert(val).expect("invalid object")) } else { None } } + pub fn as_union(&self) -> anyhow::Result { + ValueToEnum::convert(self) + } +} + +pub trait ValueToStruct { + fn convert(value: &HashMap) -> anyhow::Result where Self: Sized; +} + +pub trait ValueToEnum { + fn convert(value: &NativeUiPropertyValue) -> anyhow::Result where Self: Sized; } #[derive(Debug, Clone)] diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 606c474..7252510 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -19,7 +19,7 @@ use tonic::transport::Server; use client_context::ClientContext; use common::model::{EntrypointId, PluginId, PropertyValue, RenderLocation}; -use common::rpc::{BackendClient, RpcEntrypointTypeSearchResult, RpcEventKeyboardEvent, RpcEventRenderView, RpcEventRunCommand, RpcEventRunGeneratedCommand, RpcEventViewEvent, RpcOpenSettingsWindowPreferencesRequest, RpcRequestRunCommandRequest, RpcRequestRunGeneratedCommandRequest, RpcRequestViewRenderRequest, RpcRequestViewRenderResponseActionKind, RpcSearchRequest, RpcSendKeyboardEventRequest, RpcSendOpenEventRequest, RpcSendViewEventRequest, RpcUiPropertyValue, RpcUiWidgetId}; +use common::rpc::{BackendClient, RpcEntrypointTypeSearchResult, RpcEventKeyboardEvent, RpcEventRenderView, RpcEventRunCommand, RpcEventRunGeneratedCommand, RpcEventViewEvent, RpcOpenSettingsWindowPreferencesRequest, RpcRequestRunCommandRequest, RpcRequestRunGeneratedCommandRequest, RpcRequestViewRenderRequest, RpcRequestViewRenderResponseActionKind, RpcSearchRequest, RpcSendKeyboardEventRequest, RpcSendOpenEventRequest, RpcSendViewEventRequest, RpcUiPropertyValue, RpcUiPropertyValueObject, RpcUiWidgetId}; use common::rpc::rpc_backend_client::RpcBackendClient; use common::rpc::rpc_frontend_server::RpcFrontendServer; use common::rpc::rpc_ui_property_value::Value; @@ -376,14 +376,7 @@ impl Application for AppModel { let widget_id = RpcUiWidgetId { value: widget_id }; let event_arguments = event_arguments .into_iter() - .map(|value| match value { - PropertyValue::Bytes(value) => RpcUiPropertyValue { value: Some(Value::Bytes(value)) }, - PropertyValue::String(value) => RpcUiPropertyValue { value: Some(Value::String(value)) }, - PropertyValue::Number(value) => RpcUiPropertyValue { value: Some(Value::Number(value)) }, - PropertyValue::Bool(value) => RpcUiPropertyValue { value: Some(Value::Bool(value)) }, - PropertyValue::Json(value) => RpcUiPropertyValue { value: Some(Value::Json(value)) }, - PropertyValue::Undefined => RpcUiPropertyValue { value: Some(Value::Undefined(0)) }, - }) + .map(|value| convert_property_value(value)) .collect(); let event = RpcEventViewEvent { @@ -797,6 +790,23 @@ impl AppModel { } } +fn convert_property_value(value: PropertyValue) -> RpcUiPropertyValue { + match value { + PropertyValue::Bytes(value) => RpcUiPropertyValue { value: Some(Value::Bytes(value)) }, + PropertyValue::String(value) => RpcUiPropertyValue { value: Some(Value::String(value)) }, + PropertyValue::Number(value) => RpcUiPropertyValue { value: Some(Value::Number(value)) }, + PropertyValue::Bool(value) => RpcUiPropertyValue { value: Some(Value::Bool(value)) }, + PropertyValue::Object(value) => { + let value: HashMap = value.into_iter() + .map(|(name, value)| (name, convert_property_value(value))) + .collect(); + + RpcUiPropertyValue { value: Some(Value::Object(RpcUiPropertyValueObject { value })) } + } + PropertyValue::Undefined => RpcUiPropertyValue { value: Some(Value::Undefined(0)) }, + } +} + fn register_shortcut() -> GlobalHotKeyManager { use global_hotkey::hotkey::{Code, HotKey, Modifiers}; diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 5cb953c..9708574 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::str::FromStr; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use anyhow::anyhow; use iced::{Font, Length, Padding}; use iced::alignment::Horizontal; @@ -15,13 +16,10 @@ use iced_aw::date_picker::Date; use iced_aw::floating_element::Offset; use iced_aw::helpers::{date_picker, grid, grid_row, wrap_horizontal}; use itertools::Itertools; -use serde::de::{DeserializeOwned, StdError}; -use serde::Deserialize; use common::model::PluginId; -use component_model::PropertyType; -use crate::model::{NativeUiPropertyValue, NativeUiViewEvent, NativeUiWidgetId}; +use crate::model::{NativeUiPropertyValue, NativeUiViewEvent, NativeUiWidgetId, ValueToEnum, ValueToStruct}; use crate::ui::{ActionShortcut, ActionShortcutKind}; use crate::ui::theme::{ButtonStyle, ContainerStyle, Element, TextInputStyle, TextStyle}; @@ -460,7 +458,7 @@ impl ComponentWidgetWrapper { // } // } ComponentWidget::Image { source } => { - image(Handle::from_memory(source.clone())) // FIXME really expensive clone + image(Handle::from_memory(source.data.clone())) // FIXME really expensive clone .into() } ComponentWidget::H1 { children } => { @@ -791,8 +789,8 @@ impl ComponentWidgetWrapper { .into() } ComponentWidget::EmptyView { title, description, image } => { - let image: Option> = image.clone() // FIXME really expensive clone - .map(|image| iced::widget::image(Handle::from_memory(image)).into()); + let image: Option> = image.as_ref() + .map(|image| iced::widget::image(Handle::from_memory(image.data.clone())).into()); // FIXME really expensive clone let title: Element<_> = text(title) .into(); @@ -826,7 +824,7 @@ impl ComponentWidgetWrapper { .map(|icon| { match icon { ListItemIcon::_0(bytes) => { - image(Handle::from_memory(bytes.clone())) // FIXME really expensive clone + image(Handle::from_memory(bytes.data.clone())) // FIXME really expensive clone .into() }, ListItemIcon::_1(icon) => { @@ -1540,7 +1538,7 @@ pub fn parse_date(value: &str) -> Option<(i32, u32, u32)> { fn parse_bytes_optional(properties: &HashMap, name: &str) -> anyhow::Result>> { match properties.get(name) { None => Ok(None), - Some(value) => Ok(Some(value.as_bytes().ok_or(anyhow::anyhow!("{} has to be a string", name))?.to_owned())), + Some(value) => Ok(Some(value.as_bytes().ok_or(anyhow::anyhow!("{} has to be a byte array", name))?.to_owned())), } } @@ -1563,19 +1561,31 @@ fn parse_enum>(properties: &HashMap(properties: &HashMap, name: &str) -> anyhow::Result> { + +fn parse_object_optional(properties: &HashMap, name: &str) -> anyhow::Result> { match properties.get(name) { None => Ok(None), Some(value) => { - let value = value.as_json().ok_or(anyhow::anyhow!("{} has to be a json", name))?; + let value = value.as_object().ok_or(anyhow::anyhow!("{} has to be a object", name))?; Ok(Some(value)) }, } } -fn parse_json(properties: &HashMap, name: &str, ) -> anyhow::Result { - parse_json_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) +fn parse_object(properties: &HashMap, name: &str) -> anyhow::Result { + parse_object_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) +} + +fn parse_union_optional(properties: &HashMap, name: &str) -> anyhow::Result> { + match properties.get(name) { + None => Ok(None), + Some(value) => Ok(Some(value.as_union()?)), + } +} + +fn parse_union(properties: &HashMap, name: &str) -> anyhow::Result { + parse_union_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name)) } fn icon_to_bootstrap(icon: &Icons) -> icons::Bootstrap { @@ -1801,4 +1811,28 @@ fn icon_to_bootstrap(icon: &Icons) -> icons::Bootstrap { Icons::Indent => icons::Bootstrap::Indent, Icons::Unindent => icons::Bootstrap::Unindent, } +} + +impl ValueToEnum for ListItemIcon { + fn convert(value: &NativeUiPropertyValue) -> anyhow::Result { + match value { + NativeUiPropertyValue::String(value) => Ok(ListItemIcon::_1(Icons::from_str(value)?)), + NativeUiPropertyValue::Number(_) => Err(anyhow!("unexpected type number for ListItemIcon")), + NativeUiPropertyValue::Bool(_) => Err(anyhow!("unexpected type bool for ListItemIcon")), + NativeUiPropertyValue::Bytes(_) => Err(anyhow!("unexpected type bytes for ListItemIcon")), + NativeUiPropertyValue::Object(value) => { + Ok(ListItemIcon::_0(ImageSource { + data: parse_bytes(value, "data")?, + })) + } + } + } +} + +impl ValueToStruct for ImageSource { + fn convert(value: &HashMap) -> anyhow::Result { + Ok(ImageSource { + data: parse_bytes(value, "data")?, + }) + } } \ No newline at end of file diff --git a/rust/common/src/model.rs b/rust/common/src/model.rs index 41ef020..532ad84 100644 --- a/rust/common/src/model.rs +++ b/rust/common/src/model.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use anyhow::anyhow; @@ -66,7 +67,7 @@ pub enum PropertyValue { Number(f64), Bool(bool), Bytes(Vec), - Json(String), + Object(HashMap), Undefined, } diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index cb62b13..8e65c3f 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -81,8 +81,8 @@ pub enum PropertyType { Function { arguments: Vec }, - #[serde(rename = "image_source")] - ImageSource, + #[serde(rename = "image_data")] + ImageData, #[serde(rename = "enum")] Enum { name: String @@ -462,6 +462,10 @@ fn root(children: &[&Component]) -> Component { ].into_iter().map(|s| s.to_string()).collect() }), + ( + "ImageSource".to_owned(), + SharedType::Object { items: HashMap::from([("data".to_owned(), PropertyType::ImageData)]) } + ) ]), } } @@ -607,7 +611,7 @@ pub fn create_component_model() -> Vec { "image", "Image", [ - property("source", false, PropertyType::ImageSource) + property("source", false, PropertyType::Object { name: "ImageSource".to_owned() }) ], children_none(), ); @@ -851,7 +855,7 @@ pub fn create_component_model() -> Vec { [ property("title", false, PropertyType::String), property("description", true, PropertyType::String), - property("image", true, PropertyType::ImageSource), + property("image", true, PropertyType::Object { name: "ImageSource".to_owned() }), ], children_none(), ); @@ -863,7 +867,7 @@ pub fn create_component_model() -> Vec { property("id", false, PropertyType::String), property("title", false, PropertyType::String), property("subtitle", true, PropertyType::String), - property("icon", true, PropertyType::Union { items: vec![PropertyType::ImageSource, PropertyType::Enum { name: "Icons".to_owned() }] }), + property("icon", true, PropertyType::Union { items: vec![PropertyType::Object { name: "ImageSource".to_owned() }, PropertyType::Enum { name: "Icons".to_owned() }] }), // accessories ], children_none(), diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 3127e91..2553a1e 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -4,7 +4,7 @@ use deno_core::serde_v8; use serde::{Deserialize, Serialize}; use common::model::{EntrypointId, PropertyValue}; -use common::rpc::{RpcUiPropertyValue, RpcUiWidget, RpcUiWidgetId}; +use common::rpc::{RpcUiPropertyValue, RpcUiPropertyValueObject, RpcUiWidget, RpcUiWidgetId}; use common::rpc::rpc_ui_property_value::Value; #[derive(Debug)] @@ -175,20 +175,23 @@ impl From for RpcUiWidget { } } -pub fn from_rpc_to_intermediate_value(value: RpcUiPropertyValue) -> Option { - let value = match value.value? { +pub fn from_rpc_to_intermediate_value(value: RpcUiPropertyValue) -> PropertyValue { + match value.value.unwrap() { Value::Undefined(_) => PropertyValue::Undefined, Value::String(value) => PropertyValue::String(value), Value::Number(value) => PropertyValue::Number(value), Value::Bool(value) => PropertyValue::Bool(value), Value::Bytes(value) => PropertyValue::Bytes(value), - Value::Json(value) => PropertyValue::Json(value) - }; + Value::Object(value) => { + let value = value.value.into_iter() + .map(|(name, value)| (name, from_rpc_to_intermediate_value(value))) + .collect(); - Some(value) + PropertyValue::Object(value) + } + } } - fn from_intermediate_to_rpc_properties(value: HashMap) -> HashMap { value.into_iter() .filter_map(|(key, value)| { @@ -197,7 +200,11 @@ fn from_intermediate_to_rpc_properties(value: HashMap) -> PropertyValue::Number(value) => Some((key, RpcUiPropertyValue { value: Some(Value::Number(value)) })), PropertyValue::Bool(value) => Some((key, RpcUiPropertyValue { value: Some(Value::Bool(value)) })), PropertyValue::Bytes(value) => Some((key, RpcUiPropertyValue { value: Some(Value::Bytes(value)) })), - PropertyValue::Json(value) => Some((key, RpcUiPropertyValue { value: Some(Value::Json(value)) })), + PropertyValue::Object(value) => Some((key, RpcUiPropertyValue { + value: Some(Value::Object(RpcUiPropertyValueObject { + value: from_intermediate_to_rpc_properties(value) + })) + })), PropertyValue::Undefined => None } }) diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index 307700e..51fd417 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -10,6 +10,7 @@ use anyhow::{anyhow, Context}; use deno_core::{FastString, futures, ModuleLoader, ModuleSource, ModuleSourceFuture, ModuleType, op, OpState, ResolutionKind, serde_v8, StaticModuleLoader, v8}; use deno_core::futures::{FutureExt, Stream, StreamExt}; use deno_core::futures::executor::block_on; +use deno_core::v8::{Array, GetPropertyNamesArgs, IndexFilter, KeyCollectionMode, KeyConversionMode, Object, PropertyFilter}; use deno_runtime::deno_core::ModuleSpecifier; use deno_runtime::permissions::{Permissions, PermissionsContainer, PermissionsOptions}; use deno_runtime::worker::MainWorker; @@ -26,7 +27,7 @@ use common::model::{EntrypointId, PluginId, PropertyValue, RenderLocation}; use common::rpc::{FrontendClient, RpcClearInlineViewRequest, RpcRenderLocation, RpcReplaceViewRequest, RpcShowPreferenceRequiredViewRequest, RpcUiPropertyValue, RpcUiWidgetId}; use common::rpc::rpc_frontend_client::RpcFrontendClient; use common::rpc::rpc_frontend_server::RpcFrontend; -use component_model::{Children, Component, create_component_model, Property, PropertyType}; +use component_model::{Children, Component, create_component_model, Property, PropertyType, SharedType}; use crate::model::{from_rpc_to_intermediate_value, IntermediateUiEvent, IntermediateUiWidget, JsPropertyValue, JsRenderLocation, JsUiEvent, JsUiRequestData, JsUiResponseData, JsUiWidget, PreferenceUserData, UiWidgetId}; use crate::plugins::data_db_repository::{DataDbRepository, db_entrypoint_from_str, DbPluginEntrypointType, DbPluginPreference, DbPluginPreferenceUserData, DbReadPlugin, DbReadPluginEntrypoint}; @@ -889,11 +890,15 @@ fn op_react_replace_view( // validate_child(&state, &container.widget_type, &new_child.widget_type)? // } + let Component::Root { shared_types, .. } = component_model.components.get("gauntlet:root").unwrap() else { + unreachable!() + }; + let data = JsUiRequestData::ReplaceView { entrypoint_id: EntrypointId::from_string(entrypoint_id), render_location, top_level_view, - container: from_js_to_intermediate_widget(scope, container, component_model)?, + container: from_js_to_intermediate_widget(scope, container, component_model, shared_types)?, }; match make_request(&state, data).context("ReplaceView frontend response")? { @@ -966,7 +971,7 @@ fn validate_properties(state: &Rc>, internal_name: &str, proper PropertyValue::Bytes(_) => { todo!() } - PropertyValue::Json(_) => { + PropertyValue::Object(_) => { todo!() } } @@ -1129,7 +1134,7 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { PropertyValue::Number(value) => JsPropertyValue::Number { value }, PropertyValue::Bool(value) => JsPropertyValue::Bool { value }, PropertyValue::Undefined => JsPropertyValue::Undefined, - PropertyValue::Bytes(_) | PropertyValue::Json(_) => { + PropertyValue::Bytes(_) | PropertyValue::Object(_) => { todo!() } }) @@ -1159,9 +1164,9 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { } } -fn from_js_to_intermediate_widget(scope: &mut v8::HandleScope, ui_widget: JsUiWidget, component_model: &ComponentModel) -> anyhow::Result { +fn from_js_to_intermediate_widget(scope: &mut v8::HandleScope, ui_widget: JsUiWidget, component_model: &ComponentModel, shared_types: &HashMap) -> anyhow::Result { let children = ui_widget.widget_children.into_iter() - .map(|child| from_js_to_intermediate_widget(scope, child, component_model)) + .map(|child| from_js_to_intermediate_widget(scope, child, component_model, shared_types)) .collect::>>()?; let component = component_model.components @@ -1180,7 +1185,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); + let properties = from_js_to_intermediate_properties(scope, ui_widget.widget_properties, &props, shared_types); Ok(IntermediateUiWidget { widget_id: ui_widget.widget_id, @@ -1193,9 +1198,9 @@ fn from_js_to_intermediate_widget(scope: &mut v8::HandleScope, ui_widget: JsUiWi fn from_js_to_intermediate_properties( scope: &mut v8::HandleScope, v8_properties: HashMap, - component_props: &HashMap<&String, &PropertyType> + component_props: &HashMap<&String, &PropertyType>, + shared_types: &HashMap ) -> anyhow::Result> { - let vec = v8_properties.into_iter() .filter(|(name, _)| name.as_str() != "children") .filter(|(_, value)| !value.v8_value.is_function()) @@ -1206,65 +1211,172 @@ fn from_js_to_intermediate_properties( return Err(anyhow!("unknown property encountered {:?}", name)) }; - match property_type { - PropertyType::String => { - if val.is_string() { - Ok((name, PropertyValue::String(val.to_rust_string_lossy(scope)))) - } else { - Err(anyhow!("invalid type for property {:?}, found: {:?}, expected: string", name, val.type_repr())) - } - } - PropertyType::Number => { - if val.is_number() { - Ok((name, PropertyValue::Number(val.number_value(scope).expect("expected number")))) - } else { - Err(anyhow!("invalid type for property {:?}, found: {:?}, expected: number", name, val.type_repr())) - } - } - PropertyType::Boolean => { - if val.is_boolean() { - Ok((name, PropertyValue::Bool(val.boolean_value(scope)))) - } else { - Err(anyhow!("invalid type for property {:?}, found: {:?}, expected: boolean", name, val.type_repr())) - } - } - PropertyType::Component { .. } => { - panic!("components should not be present here") - } - PropertyType::Function { .. } => { - panic!("functions are filtered out") - } - PropertyType::ImageSource => { - if val.is_array() { // TODO arraybuffer? fix when migrating to deno's op2 - Ok((name, PropertyValue::Bytes(serde_v8::from_v8(scope, val)?))) - } else { - Err(anyhow!("invalid type for property {:?}, found: {:?}, expected: string", name, val.type_repr())) - } - } - PropertyType::Enum { .. } => { - if val.is_string() { - Ok((name, PropertyValue::String(val.to_rust_string_lossy(scope)))) - } else { - Err(anyhow!("invalid type for property {:?}, found: {:?}, expected: string", name, val.type_repr())) - } - } - PropertyType::Union { .. } => { - Ok((name.clone(), PropertyValue::Json(object_to_json(scope, val).context(format!("error while reading property {}", name))?))) - } - PropertyType::Object { .. } => { - if val.is_object() { - Ok((name.clone(), PropertyValue::Json(object_to_json(scope, val).context(format!("error while reading property {}", name))?))) - } else { - Err(anyhow!("invalid type for property {:?}, found: {:?}, expected: boolean", name, val.type_repr())) - } - } - } + convert(scope, property_type, name, val, shared_types) }) .collect::>>()?; Ok(vec.into_iter().collect()) } +fn convert( + scope: &mut v8::HandleScope, + property_type: &PropertyType, + name: String, + value: v8::Local, + shared_types: &HashMap +) -> anyhow::Result<(String, PropertyValue)> { + match property_type { + PropertyType::String | PropertyType::Enum { .. } => { + if value.is_string() { + convert_string(scope, name, value) + } else { + invalid_type_err(name, value, property_type) + } + } + PropertyType::Number => { + if value.is_number() { + convert_num(scope, name, value) + } else { + invalid_type_err(name, value, property_type) + } + } + PropertyType::Boolean => { + if value.is_boolean() { + convert_boolean(scope, name, value) + } else { + invalid_type_err(name, value, property_type) + } + } + PropertyType::Component { .. } => { + panic!("components should not be present here") + } + 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::Object { name: object_name } => { + if value.is_object() { + convert_object(scope, name, value, object_name, shared_types) + } else { + invalid_type_err(name, value, property_type) + } + } + PropertyType::Union { items } => { + if value.is_string() { + match items.iter().find(|prop_type| matches!(prop_type, PropertyType::String | PropertyType::Enum { .. })) { + None => invalid_type_err(name, value, property_type), + Some(_) => convert_string(scope, name, value) + } + } else if value.is_number() { + match items.iter().find(|prop_type| matches!(prop_type, PropertyType::Number)) { + None => invalid_type_err(name, value, property_type), + Some(_) => convert_num(scope, name, value) + } + } else if value.is_boolean() { + match items.iter().find(|prop_type| matches!(prop_type, PropertyType::Boolean)) { + 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) + } + } + } +} + +fn convert_num(scope: &mut v8::HandleScope, name: String, value: v8::Local) -> anyhow::Result<(String, PropertyValue)> { + Ok((name, PropertyValue::Number(value.number_value(scope).expect("expected number")))) +} + +fn convert_string(scope: &mut v8::HandleScope, name: String, value: v8::Local) -> anyhow::Result<(String, PropertyValue)> { + Ok((name, PropertyValue::String(value.to_rust_string_lossy(scope)))) +} + +fn convert_boolean(scope: &mut v8::HandleScope, name: String, value: v8::Local) -> anyhow::Result<(String, PropertyValue)> { + Ok((name, PropertyValue::Bool(value.boolean_value(scope)))) +} + +fn convert_bytes(scope: &mut v8::HandleScope, name: String, value: v8::Local) -> anyhow::Result<(String, PropertyValue)> { + Ok((name, PropertyValue::Bytes(serde_v8::from_v8(scope, value)?))) +} + +fn convert_object(scope: &mut v8::HandleScope, name: String, value: v8::Local, object_name: &str, shared_types: &HashMap) -> anyhow::Result<(String, PropertyValue)> { + let object: v8::Local = value.try_into().context(format!("error while reading property {}", name))?; + + let props = object + .get_own_property_names(scope, GetPropertyNamesArgs { + property_filter: PropertyFilter::ONLY_ENUMERABLE | PropertyFilter::SKIP_SYMBOLS, + key_conversion: KeyConversionMode::NoNumbers, + ..Default::default() + }) + .context("error getting get_own_property_names".to_string())?; + + let mut result_obj: HashMap = HashMap::new(); + + for index in 0..props.length() { + let key = props.get_index(scope, index).unwrap(); + let value = object.get(scope, key).unwrap(); + let key = key.to_string(scope).unwrap().to_rust_string_lossy(scope); + + let property_type = match shared_types.get(object_name).unwrap() { + SharedType::Enum { .. } => unreachable!(), + SharedType::Object { items } => items.get(&key).unwrap() + }; + + let (key, value) = convert(scope, property_type, key, value, shared_types)?; + + result_obj.insert(key, value); + } + + Ok((name, PropertyValue::Object(result_obj))) +} + +fn invalid_type_err(name: String, value: v8::Local, property_type: &PropertyType) -> anyhow::Result { + Err(anyhow!("invalid type for property {:?}, found: {:?}, expected: {}", name, value.type_repr(), expected_type(property_type))) +} + +fn expected_type(prop_type: &PropertyType) -> String { + match prop_type { + PropertyType::String => "string".to_owned(), + PropertyType::Number => "number".to_owned(), + PropertyType::Boolean => "boolean".to_owned(), + PropertyType::Component { .. } => { + panic!("components should not be present here") + } + PropertyType::Function { .. } => { + panic!("functions are filtered out") + } + PropertyType::ImageData => "bytearray".to_owned(), + PropertyType::Enum { .. } => "enum".to_owned(), + PropertyType::Union { items } => { + items.into_iter() + .map(|prop_type| expected_type(prop_type)) + .collect::>() + .join(", ") + }, + PropertyType::Object { .. } => "object".to_owned(), + } +} + fn object_to_json( scope: &mut v8::HandleScope, val: v8::Local diff --git a/rust/server/src/rpc.rs b/rust/server/src/rpc.rs index 7ec9287..b4315e0 100644 --- a/rust/server/src/rpc.rs +++ b/rust/server/src/rpc.rs @@ -113,8 +113,7 @@ impl RpcBackend for RpcBackendServerImpl { let event_arguments = event_arguments.into_iter() .map(|arg| from_rpc_to_intermediate_value(arg)) - .collect::>>() - .ok_or(Status::invalid_argument("event_arguments"))?; + .collect::>(); self.application_manager.handle_view_event(PluginId::from_string(plugin_id), widget_id, event_name, event_arguments); Ok(Response::new(RpcSendViewEventResponse::default())) diff --git a/schema/shared.proto b/schema/shared.proto index bd56826..7104846 100644 --- a/schema/shared.proto +++ b/schema/shared.proto @@ -18,7 +18,10 @@ message RpcUiPropertyValue { double number = 3; bool bool = 4; bytes bytes = 5; - string json = 6; + RpcUiPropertyValueObject object = 6; } } +message RpcUiPropertyValueObject { + map value = 1; +} \ No newline at end of file