Remove workaround for imagesource being an object

This commit is contained in:
Exidex 2024-04-14 13:09:05 +02:00
parent 844166d56b
commit 1024f2bc20
16 changed files with 367 additions and 159 deletions

2
Cargo.lock generated
View file

@ -850,8 +850,6 @@ dependencies = [
"iced",
"iced_aw",
"itertools 0.12.1",
"serde",
"serde_json",
"strum 0.26.2",
"thiserror",
"tokio",

View file

@ -42,9 +42,7 @@ declare global {
children?: ElementComponent<typeof MetadataTagList | typeof MetadataLink | typeof MetadataValue | typeof MetadataIcon | typeof MetadataSeparator>;
};
["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<typeof ListItem>;
@ -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<ImageProps> = (props: ImageProps): ReactNode => {
return <gauntlet:image source={props.source}/>;
@ -623,9 +618,7 @@ Inline.Center = Content;
export interface EmptyViewProps {
title: string;
description?: string;
image?: {
data: ArrayBuffer;
};
image?: ImageSource;
}
export const EmptyView: FC<EmptyViewProps> = (props: EmptyViewProps): ReactNode => {
return <gauntlet:empty_view title={props.title} description={props.description} image={props.image}/>;
@ -634,9 +627,7 @@ export interface ListItemProps {
id: string;
title: string;
subtitle?: string;
icon?: {
data: ArrayBuffer;
} | Icons;
icon?: ImageSource | Icons;
}
export const ListItem: FC<ListItemProps> = (props: ListItemProps): ReactNode => {
return <gauntlet:list_item id={props.id} title={props.title} subtitle={props.subtitle} icon={props.icon}/>;

View file

@ -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(

View file

@ -111,6 +111,8 @@ 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)
@ -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]
}
}
}
}

View file

@ -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"

View file

@ -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]

View file

@ -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<u8>".to_owned(),
PropertyType::ImageData => "Vec<u8>".to_owned(),
PropertyType::Union { .. } => union_name,
PropertyType::Object { name } => name.to_owned(),
PropertyType::Enum { name } => name.to_owned()

View file

@ -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<String, RpcUiPropertyValue>) -> anyhow::Result<Ha
Value::Number(value) => 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<String, RpcUiPropertyValue>) -> anyhow::Result<Ha
Ok(result)
}
fn property_value_from_rpc(map: HashMap<String, RpcUiPropertyValue>) -> HashMap<String, NativeUiPropertyValue> {
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<u8>),
Json(String),
Object(HashMap<String, NativeUiPropertyValue>),
}
impl NativeUiPropertyValue {
@ -126,13 +143,24 @@ impl NativeUiPropertyValue {
None
}
}
pub fn as_json<T: DeserializeOwned>(&self) -> Option<T> {
if let NativeUiPropertyValue::Json(val) = self {
Some(serde_json::from_str(val).expect("invalid json sent from backend?"))
pub fn as_object<T: ValueToStruct>(&self) -> Option<T> {
if let NativeUiPropertyValue::Object(val) = self {
Some(ValueToStruct::convert(val).expect("invalid object"))
} else {
None
}
}
pub fn as_union<T: ValueToEnum>(&self) -> anyhow::Result<T> {
ValueToEnum::convert(self)
}
}
pub trait ValueToStruct {
fn convert(value: &HashMap<String, NativeUiPropertyValue>) -> anyhow::Result<Self> where Self: Sized;
}
pub trait ValueToEnum {
fn convert(value: &NativeUiPropertyValue) -> anyhow::Result<Self> where Self: Sized;
}
#[derive(Debug, Clone)]

View file

@ -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<String, _> = 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};

View file

@ -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<Element<_>> = image.clone() // FIXME really expensive clone
.map(|image| iced::widget::image(Handle::from_memory(image)).into());
let image: Option<Element<_>> = 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<String, NativeUiPropertyValue>, name: &str) -> anyhow::Result<Option<Vec<u8>>> {
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<T: FromStr<Err = strum::ParseError>>(properties: &HashMap<String,
parse_enum_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name))
}
fn parse_json_optional<T: DeserializeOwned>(properties: &HashMap<String, NativeUiPropertyValue>, name: &str) -> anyhow::Result<Option<T>> {
fn parse_object_optional<T: ValueToStruct>(properties: &HashMap<String, NativeUiPropertyValue>, name: &str) -> anyhow::Result<Option<T>> {
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<T: DeserializeOwned>(properties: &HashMap<String, NativeUiPropertyValue>, name: &str, ) -> anyhow::Result<T> {
parse_json_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name))
fn parse_object<T: ValueToStruct>(properties: &HashMap<String, NativeUiPropertyValue>, name: &str) -> anyhow::Result<T> {
parse_object_optional(properties, name)?.ok_or(anyhow::anyhow!("{} is required", name))
}
fn parse_union_optional<T: ValueToEnum>(properties: &HashMap<String, NativeUiPropertyValue>, name: &str) -> anyhow::Result<Option<T>> {
match properties.get(name) {
None => Ok(None),
Some(value) => Ok(Some(value.as_union()?)),
}
}
fn parse_union<T: ValueToEnum>(properties: &HashMap<String, NativeUiPropertyValue>, name: &str) -> anyhow::Result<T> {
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<ListItemIcon> {
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<String, NativeUiPropertyValue>) -> anyhow::Result<ImageSource> {
Ok(ImageSource {
data: parse_bytes(value, "data")?,
})
}
}

View file

@ -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<u8>),
Json(String),
Object(HashMap<String, PropertyValue>),
Undefined,
}

View file

@ -81,8 +81,8 @@ pub enum PropertyType {
Function {
arguments: Vec<Property>
},
#[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<Component> {
"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<Component> {
[
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<Component> {
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(),

View file

@ -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<IntermediateUiWidget> for RpcUiWidget {
}
}
pub fn from_rpc_to_intermediate_value(value: RpcUiPropertyValue) -> Option<PropertyValue> {
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<String, PropertyValue>) -> HashMap<String, RpcUiPropertyValue> {
value.into_iter()
.filter_map(|(key, value)| {
@ -197,7 +200,11 @@ fn from_intermediate_to_rpc_properties(value: HashMap<String, PropertyValue>) ->
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
}
})

View file

@ -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<RefCell<OpState>>, 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<IntermediateUiWidget> {
fn from_js_to_intermediate_widget(scope: &mut v8::HandleScope, ui_widget: JsUiWidget, component_model: &ComponentModel, shared_types: &HashMap<String, SharedType>) -> anyhow::Result<IntermediateUiWidget> {
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::<anyhow::Result<Vec<IntermediateUiWidget>>>()?;
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::<HashMap<_, _>>();
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<String, serde_v8::Value>,
component_props: &HashMap<&String, &PropertyType>
component_props: &HashMap<&String, &PropertyType>,
shared_types: &HashMap<String, SharedType>
) -> anyhow::Result<HashMap<String, PropertyValue>> {
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::<anyhow::Result<Vec<(_, _)>>>()?;
Ok(vec.into_iter().collect())
}
fn convert(
scope: &mut v8::HandleScope,
property_type: &PropertyType,
name: String,
value: v8::Local<v8::Value>,
shared_types: &HashMap<String, SharedType>
) -> 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<v8::Value>) -> 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<v8::Value>) -> 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<v8::Value>) -> anyhow::Result<(String, PropertyValue)> {
Ok((name, PropertyValue::Bool(value.boolean_value(scope))))
}
fn convert_bytes(scope: &mut v8::HandleScope, name: String, value: v8::Local<v8::Value>) -> 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<v8::Value>, object_name: &str, shared_types: &HashMap<String, SharedType>) -> anyhow::Result<(String, PropertyValue)> {
let object: v8::Local<Object> = 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<String, PropertyValue> = 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<T>(name: String, value: v8::Local<v8::Value>, property_type: &PropertyType) -> anyhow::Result<T> {
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::<Vec<_>>()
.join(", ")
},
PropertyType::Object { .. } => "object".to_owned(),
}
}
fn object_to_json(
scope: &mut v8::HandleScope,
val: v8::Local<v8::Value>

View file

@ -113,8 +113,7 @@ impl RpcBackend for RpcBackendServerImpl {
let event_arguments = event_arguments.into_iter()
.map(|arg| from_rpc_to_intermediate_value(arg))
.collect::<Option<Vec<_>>>()
.ok_or(Status::invalid_argument("event_arguments"))?;
.collect::<Vec<_>>();
self.application_manager.handle_view_event(PluginId::from_string(plugin_id), widget_id, event_name, event_arguments);
Ok(Response::new(RpcSendViewEventResponse::default()))

View file

@ -18,7 +18,10 @@ message RpcUiPropertyValue {
double number = 3;
bool bool = 4;
bytes bytes = 5;
string json = 6;
RpcUiPropertyValueObject object = 6;
}
}
message RpcUiPropertyValueObject {
map<string, RpcUiPropertyValue> value = 1;
}