1#![cfg_attr(docsrs, feature(doc_cfg))]
51
52#[cfg(feature = "stream")]
53use std::future::ready;
54#[cfg(any(feature = "api", feature = "stream"))]
55use std::sync::Arc;
56
57mod auth;
58mod events;
59mod proto;
60
61use crate::proto::social::mixi::application::{
62 r#const::v1::{LanguageCode, PostPublishingType},
63 model::v1::PostMask,
64 service::application_api::v1::{
65 self as application_api_v1, AddStampToPostRequest, CreatePostRequest, DeletePostRequest,
66 GetStampsRequest, InitiatePostMediaUploadRequest, SendChatMessageRequest,
67 },
68};
69#[cfg(feature = "api")]
70use crate::proto::social::mixi::application::service::application_api::v1::{
71 AddStampToPostResponse, CreatePostResponse, DeletePostResponse, GetPostMediaStatusRequest,
72 GetPostMediaStatusResponse, GetPostsRequest, GetPostsResponse, GetStampsResponse,
73 GetUsersRequest, GetUsersResponse, InitiatePostMediaUploadResponse,
74 SendChatMessageResponse,
75 application_service_client::ApplicationServiceClient as RawApiClient,
76};
77#[cfg(feature = "stream")]
78use crate::proto::social::mixi::application::service::application_stream::v1::application_service_client::ApplicationServiceClient as RawStreamClient;
79#[cfg(feature = "api")]
80use crate::auth::AuthError as AuthLayerError;
81#[cfg(any(feature = "api", feature = "stream"))]
82use crate::auth::Authenticator as AuthenticatorTrait;
83#[cfg(feature = "stream")]
84use crate::events::StreamWatcher as EventStreamWatcher;
85use thiserror::Error;
86#[cfg(feature = "api")]
87use tonic::{
88 IntoRequest, Request, Response, Status,
89 body::Body as TransportBody,
90 client::GrpcService,
91 codegen::{Body, Bytes as TonicBytes, StdError},
92};
93#[cfg(any(feature = "api", feature = "stream"))]
94use tonic::transport::{Channel, Endpoint, Error as TransportError};
95
96#[cfg(feature = "api")]
97pub const DEFAULT_API_ENDPOINT: &str = "https://application-api.mixi.social";
102
103#[cfg(feature = "stream")]
104pub const DEFAULT_STREAM_ENDPOINT: &str = "https://application-stream.mixi.social";
109
110pub use crate::auth::{AuthError, Authenticator};
111#[cfg(feature = "client-credentials-auth")]
112pub use crate::auth::{AuthenticatorBuilder, ClientCredentialsAuthenticator, DEFAULT_TOKEN_URL};
113#[cfg(any(feature = "stream", feature = "webhook-core", feature = "testutil"))]
114pub use crate::events::BoxError;
115#[cfg(any(feature = "stream", feature = "webhook-core", feature = "testutil"))]
116pub use crate::events::EventHandler;
117#[cfg(feature = "webhook-axum")]
118pub use crate::events::WebhookServer;
119#[cfg(feature = "testutil")]
120pub use crate::events::testutil;
121#[cfg(feature = "webhook-core")]
122pub use crate::events::{DispatchMode, WebhookError, WebhookService};
123#[cfg(feature = "stream")]
124pub use crate::events::{StreamWatcher, StreamWatcherError};
125pub use crate::proto::{FILE_DESCRIPTOR_SET, social};
126
127#[derive(Clone, Debug, Eq, Error, PartialEq)]
129pub enum RequestValidationError {
130 #[error("in_reply_to_post_id and quoted_post_id cannot both be set")]
131 ConflictingPostTargets,
132 #[error("media_id_list can contain at most 4 entries")]
133 TooManyMediaIds,
134 #[error("room_id must not be empty")]
135 EmptyRoomId,
136 #[error("post_id must not be empty")]
137 EmptyPostId,
138 #[error("stamp_id must not be empty")]
139 EmptyStampId,
140 #[error("send chat message requires text or media_id")]
141 MissingChatPayload,
142 #[error("content_type must not be empty")]
143 EmptyContentType,
144 #[error("data_size must be greater than zero")]
145 EmptyUploadSize,
146 #[error("media_type must not be unspecified")]
147 UnspecifiedUploadType,
148}
149
150#[cfg(any(feature = "api", feature = "stream"))]
151#[derive(Debug, Error)]
153pub enum ClientBuildError {
154 #[error("channel and endpoint are mutually exclusive")]
155 ConflictingTransport,
156 #[error("failed to configure transport endpoint")]
157 Transport(#[source] TransportError),
158}
159
160#[cfg(feature = "api")]
161pub struct ApiClient<T> {
163 authenticator: Arc<dyn AuthenticatorTrait>,
164 inner: RawApiClient<T>,
165}
166
167#[cfg(feature = "api")]
168impl<T> ApiClient<T>
169where
170 T: GrpcService<TransportBody> + Send + Sync,
171 T::Error: Into<StdError>,
172 T::Future: Send,
173 T::ResponseBody: Body<Data = TonicBytes> + Send + 'static,
174 <T::ResponseBody as Body>::Error: Into<StdError> + Send,
175{
176 #[must_use]
178 pub fn new(inner: RawApiClient<T>, authenticator: Arc<dyn AuthenticatorTrait>) -> Self {
179 Self {
180 authenticator,
181 inner,
182 }
183 }
184
185 #[must_use]
187 pub const fn inner(&self) -> &RawApiClient<T> {
188 &self.inner
189 }
190
191 #[must_use]
193 pub const fn inner_mut(&mut self) -> &mut RawApiClient<T> {
194 &mut self.inner
195 }
196
197 #[must_use]
199 pub fn into_inner(self) -> RawApiClient<T> {
200 self.inner
201 }
202
203 pub async fn get_users(
210 &mut self,
211 request: impl IntoRequest<GetUsersRequest> + Send,
212 ) -> Result<Response<GetUsersResponse>, Status> {
213 let request = self.authorize_request(request).await?;
214 self.inner.get_users(request).await
215 }
216
217 pub async fn get_posts(
224 &mut self,
225 request: impl IntoRequest<GetPostsRequest> + Send,
226 ) -> Result<Response<GetPostsResponse>, Status> {
227 let request = self.authorize_request(request).await?;
228 self.inner.get_posts(request).await
229 }
230
231 pub async fn create_post(
238 &mut self,
239 request: impl IntoRequest<CreatePostRequest> + Send,
240 ) -> Result<Response<CreatePostResponse>, Status> {
241 let request = self.authorize_request(request).await?;
242 self.inner.create_post(request).await
243 }
244
245 pub async fn delete_post(
252 &mut self,
253 request: impl IntoRequest<DeletePostRequest> + Send,
254 ) -> Result<Response<DeletePostResponse>, Status> {
255 let request = self.authorize_request(request).await?;
256 self.inner.delete_post(request).await
257 }
258
259 pub async fn initiate_post_media_upload(
266 &mut self,
267 request: impl IntoRequest<InitiatePostMediaUploadRequest> + Send,
268 ) -> Result<Response<InitiatePostMediaUploadResponse>, Status> {
269 let request = self.authorize_request(request).await?;
270 self.inner.initiate_post_media_upload(request).await
271 }
272
273 pub async fn get_post_media_status(
280 &mut self,
281 request: impl IntoRequest<GetPostMediaStatusRequest> + Send,
282 ) -> Result<Response<GetPostMediaStatusResponse>, Status> {
283 let request = self.authorize_request(request).await?;
284 self.inner.get_post_media_status(request).await
285 }
286
287 pub async fn send_chat_message(
294 &mut self,
295 request: impl IntoRequest<SendChatMessageRequest> + Send,
296 ) -> Result<Response<SendChatMessageResponse>, Status> {
297 let request = self.authorize_request(request).await?;
298 self.inner.send_chat_message(request).await
299 }
300
301 pub async fn get_stamps(
308 &mut self,
309 request: impl IntoRequest<GetStampsRequest> + Send,
310 ) -> Result<Response<GetStampsResponse>, Status> {
311 let request = self.authorize_request(request).await?;
312 self.inner.get_stamps(request).await
313 }
314
315 pub async fn add_stamp_to_post(
322 &mut self,
323 request: impl IntoRequest<AddStampToPostRequest> + Send,
324 ) -> Result<Response<AddStampToPostResponse>, Status> {
325 let request = self.authorize_request(request).await?;
326 self.inner.add_stamp_to_post(request).await
327 }
328
329 async fn authorize_request<R: Send>(
330 &self,
331 request: impl IntoRequest<R> + Send,
332 ) -> Result<Request<R>, Status> {
333 let mut request = request.into_request();
334 self.authenticator
335 .authorize(request.metadata_mut())
336 .await
337 .map_err(|error| auth_error_to_status(&error))?;
338 Ok(request)
339 }
340}
341
342#[cfg(feature = "api")]
343pub struct ApiClientBuilder {
345 authenticator: Arc<dyn AuthenticatorTrait>,
346 channel: Option<Channel>,
347 endpoint: Option<String>,
348}
349
350#[cfg(feature = "api")]
351impl ApiClientBuilder {
352 #[must_use]
354 pub fn new(authenticator: Arc<dyn AuthenticatorTrait>) -> Self {
355 Self {
356 authenticator,
357 channel: None,
358 endpoint: None,
359 }
360 }
361
362 #[must_use]
364 pub fn with_channel(mut self, channel: Channel) -> Self {
365 self.channel = Some(channel);
366 self
367 }
368
369 #[must_use]
371 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
372 self.endpoint = Some(endpoint.into());
373 self
374 }
375
376 pub async fn build(self) -> Result<ApiClient<Channel>, ClientBuildError> {
383 let channel = resolve_channel(self.channel, self.endpoint, DEFAULT_API_ENDPOINT).await?;
384 let raw_client = RawApiClient::new(channel);
385 Ok(ApiClient::new(raw_client, self.authenticator))
386 }
387}
388
389#[cfg(feature = "stream")]
390pub struct StreamClientBuilder {
392 authenticator: Arc<dyn AuthenticatorTrait>,
393 channel: Option<Channel>,
394 endpoint: Option<String>,
395}
396
397#[cfg(feature = "stream")]
398impl StreamClientBuilder {
399 #[must_use]
401 pub fn new(authenticator: Arc<dyn AuthenticatorTrait>) -> Self {
402 Self {
403 authenticator,
404 channel: None,
405 endpoint: None,
406 }
407 }
408
409 #[must_use]
411 pub fn with_channel(mut self, channel: Channel) -> Self {
412 self.channel = Some(channel);
413 self
414 }
415
416 #[must_use]
418 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
419 self.endpoint = Some(endpoint.into());
420 self
421 }
422
423 pub async fn build(self) -> Result<EventStreamWatcher, ClientBuildError> {
430 let watcher = match (self.channel, self.endpoint) {
431 (Some(_), Some(_)) => Err(ClientBuildError::ConflictingTransport),
432 (Some(channel), None) => {
433 let raw_client = RawStreamClient::new(channel);
434 Ok(EventStreamWatcher::new(raw_client, self.authenticator))
435 }
436 (None, endpoint) => {
437 let endpoint = Endpoint::new(resolve_endpoint(endpoint, DEFAULT_STREAM_ENDPOINT))
438 .map_err(ClientBuildError::Transport)?;
439 let raw_client = events::http_stream_client(endpoint.uri().clone());
440 Ok(EventStreamWatcher::new(raw_client, self.authenticator))
441 }
442 };
443
444 ready(watcher).await
445 }
446}
447
448#[derive(Clone, Debug)]
450pub struct CreatePostRequestBuilder {
451 request: CreatePostRequest,
452}
453
454impl CreatePostRequestBuilder {
455 #[must_use]
457 pub fn new(text: impl Into<String>) -> Self {
458 Self {
459 request: CreatePostRequest {
460 text: text.into(),
461 in_reply_to_post_id: None,
462 quoted_post_id: None,
463 media_id_list: Vec::new(),
464 post_mask: None,
465 publishing_type: None,
466 },
467 }
468 }
469
470 #[must_use]
472 pub fn in_reply_to_post_id(mut self, post_id: impl Into<String>) -> Self {
473 self.request.in_reply_to_post_id = Some(post_id.into());
474 self
475 }
476
477 #[must_use]
479 pub fn quoted_post_id(mut self, post_id: impl Into<String>) -> Self {
480 self.request.quoted_post_id = Some(post_id.into());
481 self
482 }
483
484 #[must_use]
486 pub fn push_media_id(mut self, media_id: impl Into<String>) -> Self {
487 self.request.media_id_list.push(media_id.into());
488 self
489 }
490
491 #[must_use]
493 pub fn media_ids<I, S>(mut self, media_ids: I) -> Self
494 where
495 I: IntoIterator<Item = S>,
496 S: Into<String>,
497 {
498 self.request.media_id_list = media_ids.into_iter().map(Into::into).collect();
499 self
500 }
501
502 #[must_use]
504 pub fn post_mask(mut self, post_mask: PostMask) -> Self {
505 self.request.post_mask = Some(post_mask);
506 self
507 }
508
509 #[must_use]
511 pub const fn publishing_type(mut self, publishing_type: PostPublishingType) -> Self {
512 self.request.publishing_type = Some(publishing_type as i32);
513 self
514 }
515
516 pub fn build(self) -> Result<CreatePostRequest, RequestValidationError> {
523 if self.request.in_reply_to_post_id.is_some() && self.request.quoted_post_id.is_some() {
524 return Err(RequestValidationError::ConflictingPostTargets);
525 }
526 if self.request.media_id_list.len() > 4 {
527 return Err(RequestValidationError::TooManyMediaIds);
528 }
529 Ok(self.request)
530 }
531}
532
533#[derive(Clone, Debug)]
535pub struct DeletePostRequestBuilder {
536 request: DeletePostRequest,
537}
538
539impl DeletePostRequestBuilder {
540 #[must_use]
542 pub fn new(post_id: impl Into<String>) -> Self {
543 Self {
544 request: DeletePostRequest {
545 post_id: post_id.into(),
546 },
547 }
548 }
549
550 #[must_use]
552 pub fn post_id(mut self, post_id: impl Into<String>) -> Self {
553 self.request.post_id = post_id.into();
554 self
555 }
556
557 pub fn build(self) -> Result<DeletePostRequest, RequestValidationError> {
563 if self.request.post_id.is_empty() {
564 return Err(RequestValidationError::EmptyPostId);
565 }
566 Ok(self.request)
567 }
568}
569
570#[derive(Clone, Debug)]
572pub struct SendChatMessageRequestBuilder {
573 request: SendChatMessageRequest,
574}
575
576impl SendChatMessageRequestBuilder {
577 #[must_use]
579 pub fn new(room_id: impl Into<String>) -> Self {
580 Self {
581 request: SendChatMessageRequest {
582 room_id: room_id.into(),
583 text: None,
584 media_id: None,
585 },
586 }
587 }
588
589 #[must_use]
591 pub fn text(mut self, text: impl Into<String>) -> Self {
592 self.request.text = Some(text.into());
593 self
594 }
595
596 #[must_use]
598 pub fn media_id(mut self, media_id: impl Into<String>) -> Self {
599 self.request.media_id = Some(media_id.into());
600 self
601 }
602
603 pub fn build(self) -> Result<SendChatMessageRequest, RequestValidationError> {
609 if self.request.room_id.is_empty() {
610 return Err(RequestValidationError::EmptyRoomId);
611 }
612 if self.request.text.is_none() && self.request.media_id.is_none() {
613 return Err(RequestValidationError::MissingChatPayload);
614 }
615 Ok(self.request)
616 }
617}
618
619#[derive(Clone, Debug)]
621pub struct InitiatePostMediaUploadRequestBuilder {
622 request: InitiatePostMediaUploadRequest,
623}
624
625impl InitiatePostMediaUploadRequestBuilder {
626 #[must_use]
628 pub fn new(
629 content_type: impl Into<String>,
630 data_size: u64,
631 media_type: application_api_v1::initiate_post_media_upload_request::Type,
632 ) -> Self {
633 Self {
634 request: InitiatePostMediaUploadRequest {
635 content_type: content_type.into(),
636 data_size,
637 media_type: media_type as i32,
638 description: None,
639 },
640 }
641 }
642
643 #[must_use]
645 pub fn description(mut self, description: impl Into<String>) -> Self {
646 self.request.description = Some(description.into());
647 self
648 }
649
650 pub fn build(self) -> Result<InitiatePostMediaUploadRequest, RequestValidationError> {
657 if self.request.content_type.is_empty() {
658 return Err(RequestValidationError::EmptyContentType);
659 }
660 if self.request.data_size == 0 {
661 return Err(RequestValidationError::EmptyUploadSize);
662 }
663 if self.request.media_type
664 == application_api_v1::initiate_post_media_upload_request::Type::Unspecified as i32
665 {
666 return Err(RequestValidationError::UnspecifiedUploadType);
667 }
668 Ok(self.request)
669 }
670}
671
672#[derive(Clone, Copy, Debug, Default)]
674pub struct GetStampsRequestBuilder {
675 request: GetStampsRequest,
676}
677
678impl GetStampsRequestBuilder {
679 #[must_use]
681 pub const fn new() -> Self {
682 Self {
683 request: GetStampsRequest {
684 official_stamp_language: None,
685 },
686 }
687 }
688
689 #[must_use]
691 pub const fn official_stamp_language(mut self, language: LanguageCode) -> Self {
692 self.request.official_stamp_language = Some(language as i32);
693 self
694 }
695
696 #[must_use]
698 pub const fn build(self) -> GetStampsRequest {
699 self.request
700 }
701}
702
703#[derive(Clone, Debug)]
705pub struct AddStampToPostRequestBuilder {
706 request: AddStampToPostRequest,
707}
708
709impl AddStampToPostRequestBuilder {
710 #[must_use]
712 pub fn new(post_id: impl Into<String>, stamp_id: impl Into<String>) -> Self {
713 Self {
714 request: AddStampToPostRequest {
715 post_id: post_id.into(),
716 stamp_id: stamp_id.into(),
717 },
718 }
719 }
720
721 #[must_use]
723 pub fn post_id(mut self, post_id: impl Into<String>) -> Self {
724 self.request.post_id = post_id.into();
725 self
726 }
727
728 #[must_use]
730 pub fn stamp_id(mut self, stamp_id: impl Into<String>) -> Self {
731 self.request.stamp_id = stamp_id.into();
732 self
733 }
734
735 pub fn build(self) -> Result<AddStampToPostRequest, RequestValidationError> {
741 if self.request.post_id.is_empty() {
742 return Err(RequestValidationError::EmptyPostId);
743 }
744 if self.request.stamp_id.is_empty() {
745 return Err(RequestValidationError::EmptyStampId);
746 }
747 Ok(self.request)
748 }
749}
750
751#[cfg(feature = "api")]
752fn auth_error_to_status(error: &AuthLayerError) -> Status {
753 Status::unauthenticated(error.to_string())
754}
755
756#[cfg(feature = "api")]
757async fn resolve_channel(
758 channel: Option<Channel>,
759 endpoint: Option<String>,
760 default_endpoint: &str,
761) -> Result<Channel, ClientBuildError> {
762 match (channel, endpoint) {
763 (Some(_), Some(_)) => Err(ClientBuildError::ConflictingTransport),
764 (Some(channel), None) => Ok(channel),
765 (None, endpoint) => {
766 let endpoint = Endpoint::new(resolve_endpoint(endpoint, default_endpoint))
767 .map_err(ClientBuildError::Transport)?;
768 endpoint
769 .connect()
770 .await
771 .map_err(ClientBuildError::Transport)
772 }
773 }
774}
775
776#[cfg(any(feature = "api", feature = "stream"))]
777fn resolve_endpoint(endpoint: Option<String>, default_endpoint: &str) -> String {
778 endpoint.unwrap_or_else(|| default_endpoint.to_owned())
779}
780
781#[cfg(test)]
782mod tests {
783 use crate::social::mixi::application::{
784 r#const::v1::{LanguageCode, PostMaskType, PostPublishingType},
785 model::v1::PostMask,
786 service::application_api::v1::initiate_post_media_upload_request::Type as UploadType,
787 };
788
789 #[cfg(feature = "api")]
790 use super::DEFAULT_API_ENDPOINT;
791 #[cfg(feature = "stream")]
792 use super::DEFAULT_STREAM_ENDPOINT;
793 #[cfg(any(feature = "api", feature = "stream"))]
794 use super::resolve_endpoint;
795 use super::{
796 AddStampToPostRequestBuilder, CreatePostRequestBuilder, DeletePostRequestBuilder,
797 GetStampsRequestBuilder, InitiatePostMediaUploadRequestBuilder, RequestValidationError,
798 SendChatMessageRequestBuilder,
799 };
800
801 #[cfg(feature = "api")]
802 #[test]
803 fn resolve_endpoint_defaults_to_official_api_endpoint() {
804 assert_eq!(
805 resolve_endpoint(None, DEFAULT_API_ENDPOINT),
806 DEFAULT_API_ENDPOINT
807 );
808 }
809
810 #[cfg(feature = "stream")]
811 #[test]
812 fn resolve_endpoint_defaults_to_official_stream_endpoint() {
813 assert_eq!(
814 resolve_endpoint(None, DEFAULT_STREAM_ENDPOINT),
815 DEFAULT_STREAM_ENDPOINT
816 );
817 }
818
819 #[cfg(feature = "api")]
820 #[test]
821 fn resolve_endpoint_prefers_explicit_override() {
822 let override_endpoint = String::from("https://override.example.test");
823
824 assert_eq!(
825 resolve_endpoint(Some(override_endpoint.clone()), DEFAULT_API_ENDPOINT),
826 override_endpoint
827 );
828 }
829
830 #[test]
831 fn create_post_builder_rejects_conflicting_targets() {
832 let result = CreatePostRequestBuilder::new("hello")
833 .in_reply_to_post_id("reply")
834 .quoted_post_id("quote")
835 .build();
836
837 assert_eq!(result, Err(RequestValidationError::ConflictingPostTargets));
838 }
839
840 #[test]
841 fn create_post_builder_rejects_too_many_media_ids() {
842 let result = CreatePostRequestBuilder::new("hello")
843 .media_ids(["1", "2", "3", "4", "5"])
844 .build();
845
846 assert_eq!(result, Err(RequestValidationError::TooManyMediaIds));
847 }
848
849 #[test]
850 fn create_post_builder_accepts_optional_fields() {
851 let result = CreatePostRequestBuilder::new("hello")
852 .push_media_id("media-id")
853 .post_mask(PostMask {
854 mask_type: PostMaskType::Sensitive as i32,
855 caption: String::from("spoilers"),
856 })
857 .publishing_type(PostPublishingType::NotPublishing)
858 .build();
859
860 assert!(result.is_ok());
861 }
862
863 #[test]
864 fn delete_post_builder_requires_post_id() {
865 let result = DeletePostRequestBuilder::new("").build();
866
867 assert_eq!(result, Err(RequestValidationError::EmptyPostId));
868 }
869
870 #[test]
871 fn delete_post_builder_accepts_post_id() {
872 let result = DeletePostRequestBuilder::new("post-id").build();
873
874 assert!(result.is_ok());
875 }
876
877 #[test]
878 fn send_chat_message_builder_requires_payload() {
879 let result = SendChatMessageRequestBuilder::new("room-id").build();
880
881 assert_eq!(result, Err(RequestValidationError::MissingChatPayload));
882 }
883
884 #[test]
885 fn send_chat_message_builder_requires_room_id() {
886 let result = SendChatMessageRequestBuilder::new("").text("hello").build();
887
888 assert_eq!(result, Err(RequestValidationError::EmptyRoomId));
889 }
890
891 #[test]
892 fn initiate_upload_builder_requires_non_empty_content_type() {
893 let result = InitiatePostMediaUploadRequestBuilder::new("", 128, UploadType::Image).build();
894
895 assert_eq!(result, Err(RequestValidationError::EmptyContentType));
896 }
897
898 #[test]
899 fn initiate_upload_builder_requires_non_zero_size() {
900 let result =
901 InitiatePostMediaUploadRequestBuilder::new("image/png", 0, UploadType::Image).build();
902
903 assert_eq!(result, Err(RequestValidationError::EmptyUploadSize));
904 }
905
906 #[test]
907 fn initiate_upload_builder_rejects_unspecified_type() {
908 let result =
909 InitiatePostMediaUploadRequestBuilder::new("image/png", 128, UploadType::Unspecified)
910 .build();
911
912 assert_eq!(result, Err(RequestValidationError::UnspecifiedUploadType));
913 }
914
915 #[test]
916 fn get_stamps_builder_sets_optional_language() {
917 let request = GetStampsRequestBuilder::new()
918 .official_stamp_language(LanguageCode::En)
919 .build();
920
921 assert_eq!(
922 request.official_stamp_language,
923 Some(LanguageCode::En as i32)
924 );
925 }
926
927 #[test]
928 fn add_stamp_to_post_builder_requires_post_id() {
929 let result = AddStampToPostRequestBuilder::new("", "stamp-id").build();
930
931 assert_eq!(result, Err(RequestValidationError::EmptyPostId));
932 }
933
934 #[test]
935 fn add_stamp_to_post_builder_requires_stamp_id() {
936 let result = AddStampToPostRequestBuilder::new("post-id", "").build();
937
938 assert_eq!(result, Err(RequestValidationError::EmptyStampId));
939 }
940
941 #[test]
942 fn add_stamp_to_post_builder_accepts_identifiers() {
943 let result = AddStampToPostRequestBuilder::new("post-id", "stamp-id").build();
944
945 assert!(result.is_ok());
946 }
947}