iota_graphql_rpc/types/
display.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use async_graphql::*;
6use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
7use iota_indexer::{models::display::StoredDisplay, schema::display};
8use iota_json_rpc_types::IotaMoveValue;
9use iota_types::TypeTag;
10use move_core_types::annotated_value::{MoveStruct, MoveValue};
11
12use crate::{
13    data::{Db, DbConnection, QueryExecutor},
14    error::Error,
15};
16
17pub(crate) struct Display {
18    pub stored: StoredDisplay,
19}
20
21/// The set of named templates defined on-chain for the type of this object,
22/// to be handled off-chain. The server substitutes data from the object
23/// into these templates to generate a display string per template.
24#[derive(Debug, SimpleObject)]
25pub(crate) struct DisplayEntry {
26    /// The identifier for a particular template string of the Display object.
27    pub key: String,
28    /// The template string for the key with placeholder values substituted.
29    pub value: Option<String>,
30    /// An error string describing why the template could not be rendered.
31    pub error: Option<String>,
32}
33
34#[derive(thiserror::Error, Debug)]
35pub(crate) enum DisplayRenderError {
36    #[error("Display template value cannot be empty")]
37    TemplateValueEmpty,
38    #[error("Display template value of {0} exceeds maximum depth of {1}")]
39    ExceedsLookupDepth(usize, u64),
40    #[error("Vector of name {0} is not supported as a Display value")]
41    Vector(String),
42    #[error("Field '{0}' not found")]
43    FieldNotFound(String),
44    #[error("Unexpected MoveValue")]
45    UnexpectedMoveValue,
46}
47
48impl Display {
49    /// Query for a `Display` object by the type that it is displaying
50    pub(crate) async fn query(db: &Db, type_: TypeTag) -> Result<Option<Display>, Error> {
51        let stored: Option<StoredDisplay> = db
52            .execute(move |conn| {
53                conn.first(move || {
54                    use display::dsl;
55                    dsl::display.filter(
56                        dsl::object_type.eq(type_.to_canonical_string(/* with_prefix */ true)),
57                    )
58                })
59                .optional()
60            })
61            .await?;
62
63        Ok(stored.map(|stored| Display { stored }))
64    }
65
66    /// Render the fields defined by this `Display` from the contents of
67    /// `struct_`.
68    pub(crate) fn render(&self, struct_: &MoveStruct) -> Result<Vec<DisplayEntry>, Error> {
69        let event = self
70            .stored
71            .to_display_update_event()
72            .map_err(|e| Error::Internal(e.to_string()))?;
73
74        let mut rendered = vec![];
75        for entry in event.fields.contents {
76            rendered.push(match parse_template(&entry.value, struct_) {
77                Ok(v) => DisplayEntry::create_value(entry.key, v),
78                Err(e) => DisplayEntry::create_error(entry.key, e.to_string()),
79            });
80        }
81
82        Ok(rendered)
83    }
84}
85
86impl DisplayEntry {
87    pub(crate) fn create_value(key: String, value: String) -> Self {
88        Self {
89            key,
90            value: Some(value),
91            error: None,
92        }
93    }
94
95    pub(crate) fn create_error(key: String, error: String) -> Self {
96        Self {
97            key,
98            value: None,
99            error: Some(error),
100        }
101    }
102}
103
104/// Handles the PART of the grammar, defined as:
105/// PART   ::= '{' CHAIN '}'
106///          | '\{' | '\}'
107///          | [:utf8:]
108/// Defers resolution down to the IDENT to get_value_from_move_struct,
109/// and substitutes the result into the PART template.
110fn parse_template(template: &str, move_struct: &MoveStruct) -> Result<String, DisplayRenderError> {
111    let mut output = template.to_string();
112    let mut var_name = String::new();
113    let mut in_braces = false;
114    let mut escaped = false;
115
116    for ch in template.chars() {
117        match ch {
118            '\\' => {
119                escaped = true;
120                continue;
121            }
122            '{' if !escaped => {
123                in_braces = true;
124                var_name.clear();
125            }
126            '}' if !escaped => {
127                in_braces = false;
128                let value = get_value_from_move_struct(move_struct, &var_name)?;
129                output = output.replace(&format!("{{{}}}", var_name), &value.to_string());
130            }
131            _ if !escaped => {
132                if in_braces {
133                    var_name.push(ch);
134                }
135            }
136            _ => {}
137        }
138        escaped = false;
139    }
140
141    Ok(output.replace('\\', ""))
142}
143
144/// Handles the CHAIN and IDENT of the grammar, defined as:
145/// CHAIN  ::= IDENT | CHAIN '.' IDENT
146/// IDENT  ::= /* Move identifier */
147pub(crate) fn get_value_from_move_struct(
148    move_struct: &MoveStruct,
149    var_name: &str,
150) -> Result<String, DisplayRenderError> {
151    let parts: Vec<&str> = var_name.split('.').collect();
152    if parts.is_empty() {
153        return Err(DisplayRenderError::TemplateValueEmpty);
154    }
155    // todo: 10 is a carry-over from the iota-json-rpc implementation
156    // we should introduce this as a new limit on the config
157    if parts.len() > 10 {
158        return Err(DisplayRenderError::ExceedsLookupDepth(parts.len(), 10));
159    }
160
161    // update this as we iterate through the parts
162    let start_value = &MoveValue::Struct(move_struct.clone());
163
164    let result = parts
165        .iter()
166        .try_fold(start_value, |current_value, part| match current_value {
167            MoveValue::Struct(s) => s
168                .fields
169                .iter()
170                .find_map(|(id, value)| {
171                    if id.as_str() == *part {
172                        Some(value)
173                    } else {
174                        None
175                    }
176                })
177                .ok_or_else(|| DisplayRenderError::FieldNotFound(part.to_string())),
178            _ => Err(DisplayRenderError::UnexpectedMoveValue),
179        })?;
180
181    // TODO: move off dependency on IotaMoveValue
182    let iota_move_value: IotaMoveValue = result.clone().into();
183
184    match iota_move_value {
185        IotaMoveValue::Option(move_option) => match move_option.as_ref() {
186            Some(move_value) => Ok(move_value.to_string()),
187            None => Ok("".to_string()),
188        },
189        IotaMoveValue::Vector(_) => Err(DisplayRenderError::Vector(var_name.to_string())),
190        _ => Ok(iota_move_value.to_string()),
191    }
192}