diff options
Diffstat (limited to 'rust')
-rw-r--r-- | rust/src/api_server.rs | 476 | ||||
-rw-r--r-- | rust/src/database_models.rs | 19 |
2 files changed, 205 insertions, 290 deletions
diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index f3bf4748..c0787c74 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -23,6 +23,8 @@ use fatcat_api::{Api, ApiError, ContainerIdGetResponse, ContainerLookupGetRespon use futures::{self, Future}; use uuid; +type DbConn = diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>; + // Helper for calling through to handlers macro_rules! wrap_entity_handlers { ($get_fn:ident, $get_handler:ident, $get_resp:ident, $post_fn:ident, $post_handler:ident, @@ -33,16 +35,13 @@ macro_rules! wrap_entity_handlers { _context: &Context, ) -> Box<Future<Item = $get_resp, Error = ApiError> + Send> { let ret = match self.$get_handler(id.clone()) { - //Ok(Some(entity)) => Ok(entity) => $get_resp::FoundEntity(entity), - //Ok(None) => - // $get_resp::NotFound(ErrorResponse { message: format!("No such entity {}: {}", stringify!($model), id) }), Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) => $get_resp::NotFound(ErrorResponse { message: format!("No such entity {}: {}", stringify!($model), id) }), Err(Error(ErrorKind::Uuid(e), _)) => $get_resp::BadRequest(ErrorResponse { message: e.to_string() }), - Err(e) => $get_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(e) => $get_resp::GenericError(ErrorResponse { message: e.to_string() }), }; Box::new(futures::done(Ok(ret))) } @@ -52,32 +51,31 @@ macro_rules! wrap_entity_handlers { body: models::$model, _context: &Context, ) -> Box<Future<Item = $post_resp, Error = ApiError> + Send> { - // TODO: look for diesel foreign key and other type errors, return as BadRequest; other - // errors are a 500. let ret = match self.$post_handler(body) { Ok(edit) => $post_resp::CreatedEntity(edit), - Err(e) => $post_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::Diesel(e), _)) => + $post_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(e) => $post_resp::GenericError(ErrorResponse { message: e.to_string() }), }; Box::new(futures::done(Ok(ret))) } } } macro_rules! wrap_lookup_handler { - ($get_fn:ident, $handler:ident, $resp:ident, $idname:ident, $idtype:ident) => { + ($get_fn:ident, $get_handler:ident, $get_resp:ident, $idname:ident, $idtype:ident) => { fn $get_fn( &self, $idname: $idtype, _context: &Context, - ) -> Box<Future<Item = $resp, Error = ApiError> + Send> { - let ret = match self.$handler($idname) { - Ok(Some(entity)) => - $resp::FoundEntity(entity), - Ok(None) => - $resp::NotFound(ErrorResponse { message: "No such entity".to_string() }), + ) -> Box<Future<Item = $get_resp, Error = ApiError> + Send> { + let ret = match self.$get_handler($idname.clone()) { + Ok(entity) => + $get_resp::FoundEntity(entity), + Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) => + $get_resp::NotFound(ErrorResponse { message: format!("Not found: {}", $idname) }), Err(e) => - // TODO: dig in to error type here - $resp::BadRequest(ErrorResponse { message: e.to_string() }), + $get_resp::BadRequest(ErrorResponse { message: e.to_string() }), }; Box::new(futures::done(Ok(ret))) } @@ -89,17 +87,154 @@ pub struct Server { pub db_pool: ConnectionPool, } -fn container_row2entity(ident: ContainerIdentRow, rev: ContainerRevRow) -> Result<ContainerEntity> { +fn container_row2entity(ident: Option<ContainerIdentRow>, rev: ContainerRevRow) -> Result<ContainerEntity> { + let (state, ident_id, redirect_id) = match ident { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(i.id.to_string()), + i.redirect_id.map(|u| u.to_string())), + None => (None, None, None), + }; Ok(ContainerEntity { issnl: rev.issnl, publisher: rev.publisher, name: rev.name, abbrev: rev.abbrev, coden: rev.coden, - state: Some(ident.state().unwrap().shortname()), - ident: Some(ident.id.to_string()), - revision: ident.rev_id, - redirect: ident.redirect_id.map(|u| u.to_string()), + state: state, + ident: ident_id, + revision: Some(rev.id), + redirect: redirect_id, + extra: rev.extra_json, + editgroup_id: None, + }) +} + +fn creator_row2entity(ident: Option<CreatorIdentRow>, rev: CreatorRevRow) -> Result<CreatorEntity> { + let (state, ident_id, redirect_id) = match ident { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(i.id.to_string()), + i.redirect_id.map(|u| u.to_string())), + None => (None, None, None), + }; + Ok(CreatorEntity { + full_name: rev.full_name, + orcid: rev.orcid, + state: state, + ident: ident_id, + revision: Some(rev.id), + redirect: redirect_id, + editgroup_id: None, + extra: rev.extra_json, + }) +} + +fn file_row2entity(ident: Option<FileIdentRow>, rev: FileRevRow, conn: DbConn) -> Result<FileEntity> { + + let (state, ident_id, redirect_id) = match ident { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(i.id.to_string()), + i.redirect_id.map(|u| u.to_string())), + None => (None, None, None), + }; + + let releases: Vec<String> = file_release::table + .filter(file_release::file_rev.eq(rev.id)) + .get_results(&conn)? + .iter() + .map(|r: &FileReleaseRow| r.target_release_ident_id.to_string()) + .collect(); + + Ok(FileEntity { + sha1: rev.sha1, + md5: rev.md5, + size: rev.size.map(|v| v as i64), + url: rev.url, + releases: Some(releases), + state: state, + ident: ident_id, + revision: Some(rev.id), + redirect: redirect_id, + editgroup_id: None, + extra: rev.extra_json, + }) +} + +fn release_row2entity(ident: Option<ReleaseIdentRow>, rev: ReleaseRevRow, conn: DbConn) -> Result<ReleaseEntity> { + + let (state, ident_id, redirect_id) = match ident { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(i.id.to_string()), + i.redirect_id.map(|u| u.to_string())), + None => (None, None, None), + }; + + let refs: Vec<ReleaseRef> = release_ref::table + .filter(release_ref::release_rev.eq(rev.id)) + .get_results(&conn) + .expect("fetch release refs") + .iter() + .map(|r: &ReleaseRefRow| ReleaseRef { + index: r.index.clone(), + stub: r.stub.clone(), + target_release_id: r.target_release_ident_id.map(|v| v.to_string()), + }) + .collect(); + + let contribs: Vec<ReleaseContrib> = release_contrib::table + .filter(release_contrib::release_rev.eq(rev.id)) + .get_results(&conn) + .expect("fetch release refs") + .iter() + .map(|c: &ReleaseContribRow| ReleaseContrib { + index: c.index, + role: c.role.clone(), + creator_stub: c.stub.clone(), + creator_id: c.creator_ident_id.map(|v| v.to_string()), + }) + .collect(); + + Ok(ReleaseEntity { + title: rev.title, + release_type: rev.release_type, + date: rev.date + .map(|v| chrono::DateTime::from_utc(v.and_hms(0, 0, 0), chrono::Utc)), + doi: rev.doi, + isbn13: rev.isbn13, + volume: rev.volume, + pages: rev.pages, + issue: rev.issue, + container_id: rev.container_ident_id.map(|u| u.to_string()), + publisher: rev.publisher, + work_id: rev.work_ident_id.to_string(), + refs: Some(refs), + contribs: Some(contribs), + state: state, + ident: ident_id, + revision: Some(rev.id), + redirect: redirect_id, + editgroup_id: None, + extra: rev.extra_json, + }) +} + +fn work_row2entity(ident: Option<WorkIdentRow>, rev: WorkRevRow) -> Result<WorkEntity> { + let (state, ident_id, redirect_id) = match ident { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(i.id.to_string()), + i.redirect_id.map(|u| u.to_string())), + None => (None, None, None), + }; + Ok(WorkEntity { + work_type: rev.work_type, + state: state, + ident: ident_id, + revision: Some(rev.id), + redirect: redirect_id, editgroup_id: None, extra: rev.extra_json, }) @@ -111,42 +246,28 @@ impl Server { let conn = self.db_pool.get().expect("db_pool error"); let id = uuid::Uuid::parse_str(&id)?; - //let res: ::std::result::Result<(ContainerIdentRow, ContainerRevRow), _> = + // TODO: handle Deletions let (ident, rev): (ContainerIdentRow, ContainerRevRow) = container_ident::table .find(id) .inner_join(container_rev::table) .first(&conn)?; -/* - let (ident, rev) = match res { - Ok(r) => r, - Err(::diesel::result::Error::NotFound) => return Ok(None), - Err(e) => return Err(e.into()), - }; -*/ - //container_row2entity(ident, rev).map(|e| Some(e)) - container_row2entity(ident, rev) + container_row2entity(Some(ident), rev) } - fn container_lookup_get_handler(&self, issnl: String) -> Result<Option<ContainerEntity>> { + fn container_lookup_get_handler(&self, issnl: String) -> Result<ContainerEntity> { let conn = self.db_pool.get().expect("db_pool error"); - let res: ::std::result::Result<(ContainerIdentRow, ContainerRevRow), _> = + let (ident, rev): (ContainerIdentRow, ContainerRevRow) = container_ident::table .inner_join(container_rev::table) .filter(container_rev::issnl.eq(&issnl)) .filter(container_ident::is_live.eq(true)) .filter(container_ident::redirect_id.is_null()) - .first(&conn); - - let (ident, rev) = match res { - Ok(r) => r, - Err(::diesel::result::Error::NotFound) => return Ok(None), - Err(e) => return Err(e.into()), - }; + .first(&conn)?; - container_row2entity(ident, rev).map(|e| Some(e)) + container_row2entity(Some(ident), rev) } fn creator_id_get_handler(&self, id: String) -> Result<CreatorEntity> { @@ -158,47 +279,20 @@ impl Server { .inner_join(creator_rev::table) .first(&conn)?; - let entity = CreatorEntity { - full_name: rev.full_name, - orcid: rev.orcid, - state: Some(ident.state().unwrap().shortname()), - ident: Some(ident.id.to_string()), - revision: ident.rev_id, - redirect: ident.redirect_id.map(|u| u.to_string()), - editgroup_id: None, - extra: rev.extra_json, - }; - Ok(entity) + creator_row2entity(Some(ident), rev) } - fn creator_lookup_get_handler(&self, orcid: String) -> Result<Option<CreatorEntity>> { + fn creator_lookup_get_handler(&self, orcid: String) -> Result<CreatorEntity> { let conn = self.db_pool.get().expect("db_pool error"); - //let (ident, rev): (CreatorIdentRow, CreatorRevRow) = creator_ident::table - let res: ::std::result::Result<(CreatorIdentRow, CreatorRevRow), _> = creator_ident::table + let (ident, rev): (CreatorIdentRow, CreatorRevRow) = creator_ident::table .inner_join(creator_rev::table) .filter(creator_rev::orcid.eq(&orcid)) .filter(creator_ident::is_live.eq(true)) .filter(creator_ident::redirect_id.is_null()) - .first(&conn); - - let (ident, rev) = match res { - Ok(r) => r, - Err(diesel::result::Error::NotFound) => return Ok(None), - Err(e) => return Err(e.into()), - }; + .first(&conn)?; - let entity = CreatorEntity { - full_name: rev.full_name, - orcid: rev.orcid, - state: Some(ident.state().unwrap().shortname()), - ident: Some(ident.id.to_string()), - revision: ident.rev_id, - redirect: ident.redirect_id.map(|u| u.to_string()), - editgroup_id: None, - extra: rev.extra_json, - }; - Ok(Some(entity)) + creator_row2entity(Some(ident), rev) } fn file_id_get_handler(&self, id: String) -> Result<FileEntity> { @@ -210,69 +304,20 @@ impl Server { .inner_join(file_rev::table) .first(&conn)?; - let releases: Vec<String> = file_release::table - .filter(file_release::file_rev.eq(rev.id)) - .get_results(&conn) - .expect("fetch file releases") - .iter() - .map(|r: &FileReleaseRow| r.target_release_ident_id.to_string()) - .collect(); - - let entity = FileEntity { - sha1: rev.sha1, - md5: rev.md5, - size: rev.size.map(|v| v as i64), - url: rev.url, - releases: Some(releases), - state: Some(ident.state().unwrap().shortname()), - ident: Some(ident.id.to_string()), - revision: ident.rev_id.map(|v| v), - redirect: ident.redirect_id.map(|u| u.to_string()), - editgroup_id: None, - extra: rev.extra_json, - }; - Ok(entity) + file_row2entity(Some(ident), rev, conn) } - // TODO: refactor this to not be redundant with file_id_get_handler() code - fn file_lookup_get_handler(&self, sha1: String) -> Result<Option<FileEntity>> { + fn file_lookup_get_handler(&self, sha1: String) -> Result<FileEntity> { let conn = self.db_pool.get().expect("db_pool error"); - let res: ::std::result::Result<(FileIdentRow, FileRevRow), _> = file_ident::table + let (ident, rev): (FileIdentRow, FileRevRow) = file_ident::table .inner_join(file_rev::table) .filter(file_rev::sha1.eq(&sha1)) .filter(file_ident::is_live.eq(true)) .filter(file_ident::redirect_id.is_null()) - .first(&conn); - - let (ident, rev) = match res { - Ok(r) => r, - Err(diesel::result::Error::NotFound) => return Ok(None), - Err(e) => return Err(e.into()), - }; - - let releases: Vec<String> = file_release::table - .filter(file_release::file_rev.eq(rev.id)) - .get_results(&conn) - .expect("fetch file releases") - .iter() - .map(|r: &FileReleaseRow| r.target_release_ident_id.to_string()) - .collect(); + .first(&conn)?; - let entity = FileEntity { - sha1: rev.sha1, - md5: rev.md5, - size: rev.size.map(|v| v as i64), - url: rev.url, - releases: Some(releases), - state: Some(ident.state().unwrap().shortname()), - ident: Some(ident.id.to_string()), - revision: ident.rev_id.map(|v| v), - redirect: ident.redirect_id.map(|u| u.to_string()), - editgroup_id: None, - extra: rev.extra_json, - }; - Ok(Some(entity)) + file_row2entity(Some(ident), rev, conn) } fn release_id_get_handler(&self, id: String) -> Result<ReleaseEntity> { @@ -284,120 +329,20 @@ impl Server { .inner_join(release_rev::table) .first(&conn)?; - let refs: Vec<ReleaseRef> = release_ref::table - .filter(release_ref::release_rev.eq(rev.id)) - .get_results(&conn) - .expect("fetch release refs") - .iter() - .map(|r: &ReleaseRefRow| ReleaseRef { - index: r.index.clone(), - stub: r.stub.clone(), - target_release_id: r.target_release_ident_id.map(|v| v.to_string()), - }) - .collect(); - - let contribs: Vec<ReleaseContrib> = release_contrib::table - .filter(release_contrib::release_rev.eq(rev.id)) - .get_results(&conn) - .expect("fetch release refs") - .iter() - .map(|c: &ReleaseContribRow| ReleaseContrib { - index: c.index, - role: c.role.clone(), - creator_stub: c.stub.clone(), - creator_id: c.creator_ident_id.map(|v| v.to_string()), - }) - .collect(); - - let entity = ReleaseEntity { - title: rev.title, - release_type: rev.release_type, - date: rev.date - .map(|v| chrono::DateTime::from_utc(v.and_hms(0, 0, 0), chrono::Utc)), - doi: rev.doi, - isbn13: rev.isbn13, - volume: rev.volume, - pages: rev.pages, - issue: rev.issue, - container_id: rev.container_ident_id.map(|u| u.to_string()), - publisher: rev.publisher, - work_id: rev.work_ident_id.to_string(), - refs: Some(refs), - contribs: Some(contribs), - state: Some(ident.state().unwrap().shortname()), - ident: Some(ident.id.to_string()), - revision: ident.rev_id, - redirect: ident.redirect_id.map(|u| u.to_string()), - editgroup_id: None, - extra: rev.extra_json, - }; - Ok(entity) + release_row2entity(Some(ident), rev, conn) } - fn release_lookup_get_handler(&self, doi: String) -> Result<Option<ReleaseEntity>> { + fn release_lookup_get_handler(&self, doi: String) -> Result<ReleaseEntity> { let conn = self.db_pool.get().expect("db_pool error"); - let res: ::std::result::Result<(ReleaseIdentRow, ReleaseRevRow), _> = release_ident::table + let (ident, rev): (ReleaseIdentRow, ReleaseRevRow) = release_ident::table .inner_join(release_rev::table) .filter(release_rev::doi.eq(&doi)) .filter(release_ident::is_live.eq(true)) .filter(release_ident::redirect_id.is_null()) - .first(&conn); - - let (ident, rev) = match res { - Ok(r) => r, - Err(diesel::result::Error::NotFound) => return Ok(None), - Err(e) => return Err(e.into()), - }; - - let refs: Vec<ReleaseRef> = release_ref::table - .filter(release_ref::release_rev.eq(rev.id)) - .get_results(&conn) - .expect("fetch release refs") - .iter() - .map(|r: &ReleaseRefRow| ReleaseRef { - index: r.index.clone(), - stub: r.stub.clone(), - target_release_id: r.target_release_ident_id.map(|v| v.to_string()), - }) - .collect(); - - let contribs: Vec<ReleaseContrib> = release_contrib::table - .filter(release_contrib::release_rev.eq(rev.id)) - .get_results(&conn) - .expect("fetch release refs") - .iter() - .map(|c: &ReleaseContribRow| ReleaseContrib { - index: c.index, - role: c.role.clone(), - creator_stub: c.stub.clone(), - creator_id: c.creator_ident_id.map(|v| v.to_string()), - }) - .collect(); + .first(&conn)?; - let entity = ReleaseEntity { - title: rev.title, - release_type: rev.release_type, - date: rev.date - .map(|v| chrono::DateTime::from_utc(v.and_hms(0, 0, 0), chrono::Utc)), - doi: rev.doi, - isbn13: rev.isbn13, - volume: rev.volume, - pages: rev.pages, - issue: rev.issue, - container_id: rev.container_ident_id.map(|u| u.to_string()), - publisher: rev.publisher, - work_id: rev.work_ident_id.to_string(), - refs: Some(refs), - contribs: Some(contribs), - state: Some(ident.state().unwrap().shortname()), - ident: Some(ident.id.to_string()), - revision: ident.rev_id, - redirect: ident.redirect_id.map(|u| u.to_string()), - editgroup_id: None, - extra: rev.extra_json, - }; - Ok(Some(entity)) + release_row2entity(Some(ident), rev, conn) } fn work_id_get_handler(&self, id: String) -> Result<WorkEntity> { @@ -409,16 +354,7 @@ impl Server { .inner_join(work_rev::table) .first(&conn)?; - let entity = WorkEntity { - work_type: rev.work_type, - state: Some(ident.state().unwrap().shortname()), - ident: Some(ident.id.to_string()), - revision: ident.rev_id, - redirect: ident.redirect_id.map(|u| u.to_string()), - editgroup_id: None, - extra: rev.extra_json, - }; - Ok(entity) + work_row2entity(Some(ident), rev) } fn container_post_handler(&self, body: models::ContainerEntity) -> Result<EntityEdit> { @@ -447,16 +383,8 @@ impl Server { .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(body.extra) .bind::<diesel::sql_types::BigInt, _>(editgroup_id) .get_result(&conn)?; - let edit = &edit; - - Ok(EntityEdit { - editgroup_id: edit.editgroup_id, - revision: Some(edit.rev_id.unwrap()), - redirect_ident: None, - ident: edit.ident_id.to_string(), - edit_id: edit.id, - extra: edit.extra_json.clone(), - }) + + edit.to_model() } fn creator_post_handler(&self, body: models::CreatorEntity) -> Result<EntityEdit> { @@ -482,16 +410,8 @@ impl Server { .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(body.extra) .bind::<diesel::sql_types::BigInt, _>(editgroup_id) .get_result(&conn)?; - let edit = &edit; - - Ok(EntityEdit { - editgroup_id: edit.editgroup_id, - revision: Some(edit.rev_id.unwrap()), - redirect_ident: None, - ident: edit.ident_id.to_string(), - edit_id: edit.id, - extra: edit.extra_json.clone(), - }) + + edit.to_model() } fn file_post_handler(&self, body: models::FileEntity) -> Result<EntityEdit> { @@ -520,7 +440,6 @@ impl Server { .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(body.extra) .bind::<diesel::sql_types::BigInt, _>(editgroup_id) .get_result(&conn)?; - let edit = &edit; let _releases: Option<Vec<FileReleaseRow>> = match body.releases { None => None, @@ -544,14 +463,7 @@ impl Server { } }; - Ok(EntityEdit { - editgroup_id: edit.editgroup_id, - revision: Some(edit.rev_id.unwrap()), - redirect_ident: None, - ident: edit.ident_id.to_string(), - edit_id: edit.id, - extra: edit.extra_json.clone(), - }) + edit.to_model() } fn work_post_handler(&self, body: models::WorkEntity) -> Result<EntityEdit> { @@ -577,16 +489,8 @@ impl Server { .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(body.extra) .bind::<diesel::sql_types::BigInt, _>(editgroup_id) .get_result(&conn)?; - let edit = &edit; - - Ok(EntityEdit { - editgroup_id: edit.editgroup_id, - revision: Some(edit.rev_id.unwrap()), - redirect_ident: None, - ident: edit.ident_id.to_string(), - edit_id: edit.id, - extra: edit.extra_json.clone(), - }) + + edit.to_model() } fn release_post_handler(&self, body: models::ReleaseEntity) -> Result<EntityEdit> { @@ -628,7 +532,6 @@ impl Server { .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(body.extra) .bind::<diesel::sql_types::BigInt, _>(editgroup_id) .get_result(&conn)?; - let edit = &edit; let _refs: Option<Vec<ReleaseRefRow>> = match body.refs { None => None, @@ -683,14 +586,7 @@ impl Server { } }; - Ok(EntityEdit { - editgroup_id: edit.editgroup_id, - revision: Some(edit.rev_id.unwrap()), - redirect_ident: None, - ident: edit.ident_id.to_string(), - edit_id: edit.id, - extra: edit.extra_json.clone(), - }) + edit.to_model() } fn editgroup_id_get_handler(&self, id: i64) -> Result<Option<Editgroup>> { diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index 9913b7e2..b46d6ffb 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -3,6 +3,7 @@ use database_schema::*; use errors::*; use serde_json; use uuid::Uuid; +use fatcat_api::models::EntityEdit; // Ugh. I thought the whole point was to *not* do this, but: // https://github.com/diesel-rs/diesel/issues/1589 @@ -29,6 +30,10 @@ pub trait EntityIdentRow { fn state(&self) -> Result<EntityState>; } +pub trait EntityEditRow { + fn to_model(self) -> Result<EntityEdit>; +} + // Helper for constructing tables macro_rules! entity_structs { ($edit_table:expr, $edit_struct:ident, $ident_table:expr, $ident_struct:ident) => { @@ -43,6 +48,20 @@ macro_rules! entity_structs { pub extra_json: Option<serde_json::Value>, } + impl EntityEditRow for $edit_struct { + /// Go from a row (SQL model) to an API model + fn to_model(self) -> Result<EntityEdit> { + Ok(EntityEdit { + editgroup_id: self.editgroup_id, + revision: self.rev_id, + redirect_ident: self.redirect_id.map(|v| v.to_string()), + ident: self.ident_id.to_string(), + edit_id: self.id, + extra: self.extra_json, + }) + } + } + #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] #[table_name = $ident_table] pub struct $ident_struct { |