1use 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 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)]
265pub struct DynamicFieldInfo {
267 pub parent: ObjectId,
268 pub field_id: ObjectId,
269 pub dynamic_field_type: DynamicFieldType,
270 pub name_type: TypeTag,
271 pub name_value: Vec<u8>,
273 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}