1use axum::extract::{Path, Query, State};
6use iota_sdk_types::{Object, ObjectId, TypeTag, Version};
7use iota_types::{
8 iota_sdk_types_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 let indexes = state.inner().indexes().ok_or_else(RestError::not_found)?;
209 match accept {
210 AcceptFormat::Json => {}
211 _ => {
212 return Err(RestError::new(
213 axum::http::StatusCode::BAD_REQUEST,
214 "invalid accept type",
215 ));
216 }
217 }
218
219 let limit = parameters.limit();
220 let start = parameters.start();
221
222 let mut dynamic_fields = indexes
223 .dynamic_field_iter(parent.into(), start)?
224 .take(limit + 1)
225 .map(|result| {
226 result
227 .map_err(|err| {
228 RestError::new(
229 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
230 err.to_string(),
231 )
232 })
233 .and_then(|x| DynamicFieldInfo::try_from(x)?.pipe(Ok))
234 })
235 .collect::<Result<Vec<_>, _>>()?;
236
237 let cursor = if dynamic_fields.len() > limit {
238 dynamic_fields
241 .pop()
242 .unwrap()
243 .field_id
244 .pipe(ObjectId::from)
245 .pipe(Some)
246 } else {
247 None
248 };
249
250 ResponseContent::Json(dynamic_fields)
251 .pipe(|entries| Page { entries, cursor })
252 .pipe(Ok)
253}
254
255#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
256pub struct ListDynamicFieldsQueryParameters {
257 pub limit: Option<u32>,
258 pub start: Option<ObjectId>,
259}
260
261impl ListDynamicFieldsQueryParameters {
262 pub fn limit(&self) -> usize {
263 self.limit
264 .map(|l| (l as usize).clamp(1, crate::MAX_PAGE_SIZE))
265 .unwrap_or(crate::DEFAULT_PAGE_SIZE)
266 }
267
268 pub fn start(&self) -> Option<iota_types::base_types::ObjectID> {
269 self.start.map(Into::into)
270 }
271}
272
273#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, schemars::JsonSchema)]
274pub struct DynamicFieldInfo {
276 pub parent: ObjectId,
277 pub field_id: ObjectId,
278 pub dynamic_field_type: DynamicFieldType,
279 pub name_type: TypeTag,
280 pub name_value: Vec<u8>,
282 pub dynamic_object_id: Option<ObjectId>,
285}
286
287impl TryFrom<(DynamicFieldKey, DynamicFieldIndexInfo)> for DynamicFieldInfo {
288 type Error = SdkTypeConversionError;
289
290 fn try_from(value: (DynamicFieldKey, DynamicFieldIndexInfo)) -> Result<Self, Self::Error> {
291 let DynamicFieldKey { parent, field_id } = value.0;
292 let DynamicFieldIndexInfo {
293 dynamic_field_type,
294 name_type,
295 name_value,
296 dynamic_object_id,
297 } = value.1;
298
299 Self {
300 parent: parent.into(),
301 field_id: field_id.into(),
302 dynamic_field_type: dynamic_field_type.into(),
303 name_type: type_tag_core_to_sdk(name_type)?,
304 name_value,
305 dynamic_object_id: dynamic_object_id.map(Into::into),
306 }
307 .pipe(Ok)
308 }
309}
310
311#[derive(
312 Clone, Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Debug, schemars::JsonSchema,
313)]
314#[serde(rename_all = "lowercase")]
315pub enum DynamicFieldType {
316 Field,
317 Object,
318}
319
320impl From<iota_types::dynamic_field::DynamicFieldType> for DynamicFieldType {
321 fn from(value: iota_types::dynamic_field::DynamicFieldType) -> Self {
322 match value {
323 iota_types::dynamic_field::DynamicFieldType::DynamicField => Self::Field,
324 iota_types::dynamic_field::DynamicFieldType::DynamicObject => Self::Object,
325 }
326 }
327}