iota_rest_api/
objects.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use axum::extract::{Path, Query, State};
6use iota_sdk2::types::{Object, ObjectId, TypeTag, Version};
7use iota_types::{
8    iota_sdk2_conversions::{SdkTypeConversionError, type_tag_core_to_sdk},
9    storage::{DynamicFieldIndexInfo, DynamicFieldKey},
10};
11use serde::{Deserialize, Serialize};
12use tap::Pipe;
13
14use crate::{
15    Page, RestError, RestService, Result,
16    accept::AcceptFormat,
17    openapi::{ApiEndpoint, OperationBuilder, ResponseBuilder, RouteHandler},
18    reader::StateReader,
19    response::ResponseContent,
20};
21
22pub struct GetObject;
23
24impl ApiEndpoint<RestService> for GetObject {
25    fn method(&self) -> axum::http::Method {
26        axum::http::Method::GET
27    }
28
29    fn path(&self) -> &'static str {
30        "/objects/{object_id}"
31    }
32
33    fn operation(
34        &self,
35        generator: &mut schemars::gen::SchemaGenerator,
36    ) -> openapiv3::v3_1::Operation {
37        OperationBuilder::new()
38            .tag("Objects")
39            .operation_id("GetObject")
40            .path_parameter::<ObjectId>("object_id", generator)
41            .response(
42                200,
43                ResponseBuilder::new()
44                    .json_content::<Object>(generator)
45                    .bcs_content()
46                    .build(),
47            )
48            .response(404, ResponseBuilder::new().build())
49            .build()
50    }
51
52    fn handler(&self) -> crate::openapi::RouteHandler<RestService> {
53        RouteHandler::new(self.method(), get_object)
54    }
55}
56
57pub async fn get_object(
58    Path(object_id): Path<ObjectId>,
59    accept: AcceptFormat,
60    State(state): State<StateReader>,
61) -> Result<ResponseContent<Object>> {
62    let object = state
63        .get_object(object_id)?
64        .ok_or_else(|| ObjectNotFoundError::new(object_id))?;
65
66    match accept {
67        AcceptFormat::Json => ResponseContent::Json(object),
68        AcceptFormat::Bcs => ResponseContent::Bcs(object),
69    }
70    .pipe(Ok)
71}
72
73pub struct GetObjectWithVersion;
74
75impl ApiEndpoint<RestService> for GetObjectWithVersion {
76    fn method(&self) -> axum::http::Method {
77        axum::http::Method::GET
78    }
79
80    fn path(&self) -> &'static str {
81        "/objects/{object_id}/version/{version}"
82    }
83
84    fn operation(
85        &self,
86        generator: &mut schemars::gen::SchemaGenerator,
87    ) -> openapiv3::v3_1::Operation {
88        OperationBuilder::new()
89            .tag("Objects")
90            .operation_id("GetObjectWithVersion")
91            .path_parameter::<ObjectId>("object_id", generator)
92            .path_parameter::<Version>("version", generator)
93            .response(
94                200,
95                ResponseBuilder::new()
96                    .json_content::<Object>(generator)
97                    .bcs_content()
98                    .build(),
99            )
100            .response(404, ResponseBuilder::new().build())
101            .build()
102    }
103
104    fn handler(&self) -> crate::openapi::RouteHandler<RestService> {
105        RouteHandler::new(self.method(), get_object_with_version)
106    }
107}
108
109pub async fn get_object_with_version(
110    Path((object_id, version)): Path<(ObjectId, Version)>,
111    accept: AcceptFormat,
112    State(state): State<StateReader>,
113) -> Result<ResponseContent<Object>> {
114    let object = state
115        .get_object_with_version(object_id, version)?
116        .ok_or_else(|| ObjectNotFoundError::new_with_version(object_id, version))?;
117
118    match accept {
119        AcceptFormat::Json => ResponseContent::Json(object),
120        AcceptFormat::Bcs => ResponseContent::Bcs(object),
121    }
122    .pipe(Ok)
123}
124
125#[derive(Debug)]
126pub struct ObjectNotFoundError {
127    object_id: ObjectId,
128    version: Option<Version>,
129}
130
131impl ObjectNotFoundError {
132    pub fn new(object_id: ObjectId) -> Self {
133        Self {
134            object_id,
135            version: None,
136        }
137    }
138
139    pub fn new_with_version(object_id: ObjectId, version: Version) -> Self {
140        Self {
141            object_id,
142            version: Some(version),
143        }
144    }
145}
146
147impl std::fmt::Display for ObjectNotFoundError {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        write!(f, "Object {}", self.object_id)?;
150
151        if let Some(version) = self.version {
152            write!(f, " with version {version}")?;
153        }
154
155        write!(f, " not found")
156    }
157}
158
159impl std::error::Error for ObjectNotFoundError {}
160
161impl From<ObjectNotFoundError> for crate::RestError {
162    fn from(value: ObjectNotFoundError) -> Self {
163        Self::new(axum::http::StatusCode::NOT_FOUND, value.to_string())
164    }
165}
166
167pub struct ListDynamicFields;
168
169impl ApiEndpoint<RestService> for ListDynamicFields {
170    fn method(&self) -> axum::http::Method {
171        axum::http::Method::GET
172    }
173
174    fn path(&self) -> &'static str {
175        "/objects/{object_id}/dynamic-fields"
176    }
177
178    fn operation(
179        &self,
180        generator: &mut schemars::gen::SchemaGenerator,
181    ) -> openapiv3::v3_1::Operation {
182        OperationBuilder::new()
183            .tag("Objects")
184            .operation_id("ListDynamicFields")
185            .path_parameter::<ObjectId>("object_id", generator)
186            .query_parameters::<ListDynamicFieldsQueryParameters>(generator)
187            .response(
188                200,
189                ResponseBuilder::new()
190                    .json_content::<Vec<DynamicFieldInfo>>(generator)
191                    .header::<String>(crate::types::X_IOTA_CURSOR, generator)
192                    .build(),
193            )
194            .build()
195    }
196
197    fn handler(&self) -> crate::openapi::RouteHandler<RestService> {
198        RouteHandler::new(self.method(), list_dynamic_fields)
199    }
200}
201
202async fn list_dynamic_fields(
203    Path(parent): Path<ObjectId>,
204    Query(parameters): Query<ListDynamicFieldsQueryParameters>,
205    accept: AcceptFormat,
206    State(state): State<StateReader>,
207) -> Result<Page<DynamicFieldInfo, ObjectId>> {
208    match accept {
209        AcceptFormat::Json => {}
210        _ => {
211            return Err(RestError::new(
212                axum::http::StatusCode::BAD_REQUEST,
213                "invalid accept type",
214            ));
215        }
216    }
217
218    let limit = parameters.limit();
219    let start = parameters.start();
220
221    let mut dynamic_fields = state
222        .inner()
223        .dynamic_field_iter(parent.into(), start)?
224        .take(limit + 1)
225        .map(DynamicFieldInfo::try_from)
226        .collect::<Result<Vec<_>, _>>()?;
227
228    let cursor = if dynamic_fields.len() > limit {
229        // SAFETY: We've already verified that object_keys is greater than limit, which
230        // is guaranteed to be >= 1.
231        dynamic_fields
232            .pop()
233            .unwrap()
234            .field_id
235            .pipe(ObjectId::from)
236            .pipe(Some)
237    } else {
238        None
239    };
240
241    ResponseContent::Json(dynamic_fields)
242        .pipe(|entries| Page { entries, cursor })
243        .pipe(Ok)
244}
245
246#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
247pub struct ListDynamicFieldsQueryParameters {
248    pub limit: Option<u32>,
249    pub start: Option<ObjectId>,
250}
251
252impl ListDynamicFieldsQueryParameters {
253    pub fn limit(&self) -> usize {
254        self.limit
255            .map(|l| (l as usize).clamp(1, crate::MAX_PAGE_SIZE))
256            .unwrap_or(crate::DEFAULT_PAGE_SIZE)
257    }
258
259    pub fn start(&self) -> Option<iota_types::base_types::ObjectID> {
260        self.start.map(Into::into)
261    }
262}
263
264#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, schemars::JsonSchema)]
265/// DynamicFieldInfo
266pub struct DynamicFieldInfo {
267    pub parent: ObjectId,
268    pub field_id: ObjectId,
269    pub dynamic_field_type: DynamicFieldType,
270    pub name_type: TypeTag,
271    // TODO fix the json format of this type to be base64 encoded
272    pub name_value: Vec<u8>,
273    /// ObjectId of the child object when `dynamic_field_type ==
274    /// DynamicFieldType::Object`
275    pub dynamic_object_id: Option<ObjectId>,
276}
277
278impl TryFrom<(DynamicFieldKey, DynamicFieldIndexInfo)> for DynamicFieldInfo {
279    type Error = SdkTypeConversionError;
280
281    fn try_from(value: (DynamicFieldKey, DynamicFieldIndexInfo)) -> Result<Self, Self::Error> {
282        let DynamicFieldKey { parent, field_id } = value.0;
283        let DynamicFieldIndexInfo {
284            dynamic_field_type,
285            name_type,
286            name_value,
287            dynamic_object_id,
288        } = value.1;
289
290        Self {
291            parent: parent.into(),
292            field_id: field_id.into(),
293            dynamic_field_type: dynamic_field_type.into(),
294            name_type: type_tag_core_to_sdk(name_type)?,
295            name_value,
296            dynamic_object_id: dynamic_object_id.map(Into::into),
297        }
298        .pipe(Ok)
299    }
300}
301
302#[derive(
303    Clone, Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Debug, schemars::JsonSchema,
304)]
305#[serde(rename_all = "lowercase")]
306pub enum DynamicFieldType {
307    Field,
308    Object,
309}
310
311impl From<iota_types::dynamic_field::DynamicFieldType> for DynamicFieldType {
312    fn from(value: iota_types::dynamic_field::DynamicFieldType) -> Self {
313        match value {
314            iota_types::dynamic_field::DynamicFieldType::DynamicField => Self::Field,
315            iota_types::dynamic_field::DynamicFieldType::DynamicObject => Self::Object,
316        }
317    }
318}