1use aws_sdk_glue::error::{DisplayErrorContext, SdkError};
13use aws_sdk_glue::operation::create_schema::CreateSchemaError as SdkCreateSchemaError;
14use aws_sdk_glue::operation::get_registry::GetRegistryError as SdkGetRegistryError;
15use aws_sdk_glue::operation::get_schema::GetSchemaError as SdkGetSchemaError;
16use aws_sdk_glue::operation::get_schema_version::{
17 GetSchemaVersionError as SdkGetSchemaVersionError, GetSchemaVersionOutput,
18};
19use aws_sdk_glue::operation::get_schema_by_definition as sdk_get_by_def;
23use aws_sdk_glue::operation::register_schema_version as sdk_register;
24use aws_sdk_glue::types::{
25 Compatibility as SdkCompatibility, DataFormat as SdkDataFormat, RegistryId,
26 RegistryStatus as SdkRegistryStatus, SchemaId, SchemaStatus as SdkSchemaStatus,
27 SchemaVersionNumber, SchemaVersionStatus as SdkSchemaVersionStatus,
28};
29use aws_types::SdkConfig;
30use thiserror::Error;
31use uuid::Uuid;
32
33#[derive(Clone, Debug)]
38pub struct Client {
39 inner: aws_sdk_glue::Client,
40}
41
42impl Client {
43 pub(crate) fn from_sdk_config(sdk_config: SdkConfig) -> Self {
44 Client {
45 inner: aws_sdk_glue::Client::new(&sdk_config),
46 }
47 }
48
49 #[cfg(feature = "test-util")]
56 pub fn from_sdk_client(inner: aws_sdk_glue::Client) -> Self {
57 Client { inner }
58 }
59
60 pub async fn get_registry(&self, name: &str) -> Result<Registry, GetRegistryError> {
66 let id = RegistryId::builder().registry_name(name).build();
67 let output = self
68 .inner
69 .get_registry()
70 .registry_id(id)
71 .send()
72 .await
73 .map_err(classify_get_registry_error)?;
74 Ok(Registry {
75 name: output.registry_name,
76 arn: output.registry_arn,
77 description: output.description,
78 lifecycle_status: output.status.map(RegistryLifecycleStatus::from_sdk),
79 })
80 }
81
82 pub async fn get_schema_version_by_id(
101 &self,
102 id: Uuid,
103 ) -> Result<SchemaVersion, GetSchemaVersionError> {
104 let output = self
105 .inner
106 .get_schema_version()
107 .schema_version_id(id.to_string())
108 .send()
109 .await
110 .map_err(classify_get_schema_version_error)?;
111 Ok(SchemaVersion::from_sdk(output))
112 }
113
114 pub async fn get_schema_version_latest_by_name(
123 &self,
124 registry_name: &str,
125 schema_name: &str,
126 ) -> Result<SchemaVersion, GetSchemaVersionError> {
127 let schema_id = SchemaId::builder()
128 .registry_name(registry_name)
129 .schema_name(schema_name)
130 .build();
131 let version_number = SchemaVersionNumber::builder().latest_version(true).build();
132 let output = self
133 .inner
134 .get_schema_version()
135 .schema_id(schema_id)
136 .schema_version_number(version_number)
137 .send()
138 .await
139 .map_err(classify_get_schema_version_error)?;
140 Ok(SchemaVersion::from_sdk(output))
141 }
142
143 pub async fn get_schema_by_definition(
156 &self,
157 registry_name: &str,
158 schema_name: &str,
159 definition: &str,
160 ) -> Result<RegisteredSchemaVersion, GetSchemaByDefinitionError> {
161 let schema_id = SchemaId::builder()
162 .registry_name(registry_name)
163 .schema_name(schema_name)
164 .build();
165 let output = self
166 .inner
167 .get_schema_by_definition()
168 .schema_id(schema_id)
169 .schema_definition(definition)
170 .send()
171 .await
172 .map_err(classify_get_schema_by_definition_error)?;
173 let id = parse_schema_version_id(output.schema_version_id)
174 .map_err(GetSchemaByDefinitionError::Other)?;
175 Ok(RegisteredSchemaVersion {
176 id,
177 lifecycle_status: output.status.map(SchemaVersionLifecycleStatus::from_sdk),
178 })
179 }
180
181 pub async fn register_schema_version(
195 &self,
196 registry_name: &str,
197 schema_name: &str,
198 definition: &str,
199 ) -> Result<RegisteredSchemaVersion, RegisterSchemaVersionError> {
200 let schema_id = SchemaId::builder()
201 .registry_name(registry_name)
202 .schema_name(schema_name)
203 .build();
204 let output = self
205 .inner
206 .register_schema_version()
207 .schema_id(schema_id)
208 .schema_definition(definition)
209 .send()
210 .await
211 .map_err(classify_register_schema_version_error)?;
212 let id = parse_schema_version_id(output.schema_version_id)
213 .map_err(RegisterSchemaVersionError::Other)?;
214 Ok(RegisteredSchemaVersion {
215 id,
216 lifecycle_status: output.status.map(SchemaVersionLifecycleStatus::from_sdk),
217 })
218 }
219
220 pub async fn create_schema(
236 &self,
237 registry_name: &str,
238 schema_name: &str,
239 data_format: DataFormat,
240 compatibility: Compatibility,
241 definition: &str,
242 ) -> Result<RegisteredSchemaVersion, CreateSchemaError> {
243 let registry_id = RegistryId::builder().registry_name(registry_name).build();
244 let output = self
245 .inner
246 .create_schema()
247 .registry_id(registry_id)
248 .schema_name(schema_name)
249 .data_format(data_format.to_sdk())
250 .compatibility(compatibility.to_sdk())
251 .schema_definition(definition)
252 .send()
253 .await
254 .map_err(classify_create_schema_error)?;
255 let id =
256 parse_schema_version_id(output.schema_version_id).map_err(CreateSchemaError::Other)?;
257 Ok(RegisteredSchemaVersion {
258 id,
259 lifecycle_status: output
260 .schema_version_status
261 .map(SchemaVersionLifecycleStatus::from_sdk),
262 })
263 }
264
265 pub async fn get_schema(
272 &self,
273 registry_name: &str,
274 schema_name: &str,
275 ) -> Result<Schema, GetSchemaError> {
276 let schema_id = SchemaId::builder()
277 .registry_name(registry_name)
278 .schema_name(schema_name)
279 .build();
280 let output = self
281 .inner
282 .get_schema()
283 .schema_id(schema_id)
284 .send()
285 .await
286 .map_err(classify_get_schema_error)?;
287 Ok(Schema {
288 compatibility: output.compatibility.map(Compatibility::from_sdk),
289 lifecycle_status: output.schema_status.map(SchemaLifecycleStatus::from_sdk),
290 })
291 }
292}
293
294fn parse_schema_version_id(id: Option<String>) -> Result<Uuid, String> {
297 let id = id.ok_or_else(|| "Glue response missing schema version id".to_string())?;
298 Uuid::parse_str(&id).map_err(|e| format!("invalid Glue schema version id {id:?}: {e}"))
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct Registry {
307 pub name: Option<String>,
308 pub arn: Option<String>,
309 pub description: Option<String>,
310 pub lifecycle_status: Option<RegistryLifecycleStatus>,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
320pub enum RegistryLifecycleStatus {
321 Available,
322 Deleting,
323 Unknown(String),
325}
326
327impl RegistryLifecycleStatus {
328 fn from_sdk(status: SdkRegistryStatus) -> Self {
329 match &status {
330 SdkRegistryStatus::Available => RegistryLifecycleStatus::Available,
331 SdkRegistryStatus::Deleting => RegistryLifecycleStatus::Deleting,
332 _ => RegistryLifecycleStatus::Unknown(status.as_str().to_string()),
333 }
334 }
335}
336
337#[derive(Debug, Error)]
339pub enum GetRegistryError {
340 #[error("registry not found")]
343 NotFound,
344 #[error("AWS Glue error: {0}")]
347 Other(String),
348}
349
350fn classify_get_registry_error(err: SdkError<SdkGetRegistryError>) -> GetRegistryError {
351 if let SdkError::ServiceError(service_err) = &err
352 && matches!(
353 service_err.err(),
354 SdkGetRegistryError::EntityNotFoundException(_)
355 )
356 {
357 return GetRegistryError::NotFound;
358 }
359 GetRegistryError::Other(DisplayErrorContext(&err).to_string())
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct SchemaVersion {
370 pub schema_version_id: Option<String>,
371 pub schema_arn: Option<String>,
372 pub definition: Option<String>,
374 pub data_format: Option<DataFormat>,
378 pub version_number: Option<i64>,
379 pub lifecycle_status: Option<SchemaVersionLifecycleStatus>,
380}
381
382impl SchemaVersion {
383 fn from_sdk(output: GetSchemaVersionOutput) -> Self {
384 SchemaVersion {
385 schema_version_id: output.schema_version_id,
386 schema_arn: output.schema_arn,
387 definition: output.schema_definition,
388 data_format: output.data_format.map(DataFormat::from_sdk),
389 version_number: output.version_number,
390 lifecycle_status: output.status.map(SchemaVersionLifecycleStatus::from_sdk),
391 }
392 }
393}
394
395#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct RegisteredSchemaVersion {
405 pub id: Uuid,
406 pub lifecycle_status: Option<SchemaVersionLifecycleStatus>,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
415pub enum DataFormat {
416 Avro,
417 Json,
418 Protobuf,
419 Unknown(String),
421}
422
423impl DataFormat {
424 fn from_sdk(format: SdkDataFormat) -> Self {
425 match &format {
426 SdkDataFormat::Avro => DataFormat::Avro,
427 SdkDataFormat::Json => DataFormat::Json,
428 SdkDataFormat::Protobuf => DataFormat::Protobuf,
429 _ => DataFormat::Unknown(format.as_str().to_string()),
430 }
431 }
432
433 fn to_sdk(&self) -> SdkDataFormat {
434 match self {
435 DataFormat::Avro => SdkDataFormat::Avro,
436 DataFormat::Json => SdkDataFormat::Json,
437 DataFormat::Protobuf => SdkDataFormat::Protobuf,
438 DataFormat::Unknown(s) => SdkDataFormat::from(s.as_str()),
439 }
440 }
441
442 pub fn as_str(&self) -> &str {
445 match self {
446 DataFormat::Avro => "AVRO",
447 DataFormat::Json => "JSON",
448 DataFormat::Protobuf => "PROTOBUF",
449 DataFormat::Unknown(s) => s,
450 }
451 }
452}
453
454#[derive(Debug, Clone, PartialEq, Eq)]
463pub enum Compatibility {
464 None,
465 Disabled,
466 Backward,
467 BackwardAll,
468 Forward,
469 ForwardAll,
470 Full,
471 FullAll,
472 Unknown(String),
474}
475
476impl Compatibility {
477 fn from_sdk(compatibility: SdkCompatibility) -> Self {
478 match &compatibility {
479 SdkCompatibility::None => Compatibility::None,
480 SdkCompatibility::Disabled => Compatibility::Disabled,
481 SdkCompatibility::Backward => Compatibility::Backward,
482 SdkCompatibility::BackwardAll => Compatibility::BackwardAll,
483 SdkCompatibility::Forward => Compatibility::Forward,
484 SdkCompatibility::ForwardAll => Compatibility::ForwardAll,
485 SdkCompatibility::Full => Compatibility::Full,
486 SdkCompatibility::FullAll => Compatibility::FullAll,
487 _ => Compatibility::Unknown(compatibility.as_str().to_string()),
488 }
489 }
490
491 fn to_sdk(&self) -> SdkCompatibility {
492 match self {
493 Compatibility::None => SdkCompatibility::None,
494 Compatibility::Disabled => SdkCompatibility::Disabled,
495 Compatibility::Backward => SdkCompatibility::Backward,
496 Compatibility::BackwardAll => SdkCompatibility::BackwardAll,
497 Compatibility::Forward => SdkCompatibility::Forward,
498 Compatibility::ForwardAll => SdkCompatibility::ForwardAll,
499 Compatibility::Full => SdkCompatibility::Full,
500 Compatibility::FullAll => SdkCompatibility::FullAll,
501 Compatibility::Unknown(s) => SdkCompatibility::from(s.as_str()),
502 }
503 }
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
512pub struct Schema {
513 pub compatibility: Option<Compatibility>,
514 pub lifecycle_status: Option<SchemaLifecycleStatus>,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq)]
523pub enum SchemaLifecycleStatus {
524 Available,
525 Pending,
526 Deleting,
527 Unknown(String),
529}
530
531impl SchemaLifecycleStatus {
532 fn from_sdk(status: SdkSchemaStatus) -> Self {
533 match &status {
534 SdkSchemaStatus::Available => SchemaLifecycleStatus::Available,
535 SdkSchemaStatus::Pending => SchemaLifecycleStatus::Pending,
536 SdkSchemaStatus::Deleting => SchemaLifecycleStatus::Deleting,
537 _ => SchemaLifecycleStatus::Unknown(status.as_str().to_string()),
538 }
539 }
540}
541
542#[derive(Debug, Clone, PartialEq, Eq)]
547pub enum SchemaVersionLifecycleStatus {
548 Available,
549 Pending,
550 Failure,
551 Deleting,
552 Unknown(String),
554}
555
556impl SchemaVersionLifecycleStatus {
557 fn from_sdk(status: SdkSchemaVersionStatus) -> Self {
558 match &status {
559 SdkSchemaVersionStatus::Available => SchemaVersionLifecycleStatus::Available,
560 SdkSchemaVersionStatus::Pending => SchemaVersionLifecycleStatus::Pending,
561 SdkSchemaVersionStatus::Failure => SchemaVersionLifecycleStatus::Failure,
562 SdkSchemaVersionStatus::Deleting => SchemaVersionLifecycleStatus::Deleting,
563 _ => SchemaVersionLifecycleStatus::Unknown(status.as_str().to_string()),
564 }
565 }
566}
567
568#[derive(Debug, Error)]
571pub enum GetSchemaVersionError {
572 #[error("schema version not found")]
575 NotFound,
576 #[error("AWS Glue error: {0}")]
578 Other(String),
579}
580
581fn classify_get_schema_version_error(
582 err: SdkError<SdkGetSchemaVersionError>,
583) -> GetSchemaVersionError {
584 if let SdkError::ServiceError(service_err) = &err
585 && matches!(
586 service_err.err(),
587 SdkGetSchemaVersionError::EntityNotFoundException(_)
588 )
589 {
590 return GetSchemaVersionError::NotFound;
591 }
592 GetSchemaVersionError::Other(DisplayErrorContext(&err).to_string())
593}
594
595#[derive(Debug, Error)]
597pub enum GetSchemaByDefinitionError {
598 #[error("schema version for definition not found")]
602 NotFound,
603 #[error("AWS Glue error: {0}")]
606 Other(String),
607}
608
609fn classify_get_schema_by_definition_error(
610 err: SdkError<sdk_get_by_def::GetSchemaByDefinitionError>,
611) -> GetSchemaByDefinitionError {
612 if let SdkError::ServiceError(service_err) = &err
613 && matches!(
614 service_err.err(),
615 sdk_get_by_def::GetSchemaByDefinitionError::EntityNotFoundException(_)
616 )
617 {
618 return GetSchemaByDefinitionError::NotFound;
619 }
620 GetSchemaByDefinitionError::Other(DisplayErrorContext(&err).to_string())
621}
622
623#[derive(Debug, Error)]
625pub enum RegisterSchemaVersionError {
626 #[error("schema not found")]
630 SchemaNotFound,
631 #[error("AWS Glue error: {0}")]
634 Other(String),
635}
636
637fn classify_register_schema_version_error(
638 err: SdkError<sdk_register::RegisterSchemaVersionError>,
639) -> RegisterSchemaVersionError {
640 if let SdkError::ServiceError(service_err) = &err
641 && matches!(
642 service_err.err(),
643 sdk_register::RegisterSchemaVersionError::EntityNotFoundException(_)
644 )
645 {
646 return RegisterSchemaVersionError::SchemaNotFound;
647 }
648 RegisterSchemaVersionError::Other(DisplayErrorContext(&err).to_string())
649}
650
651#[derive(Debug, Error)]
653pub enum CreateSchemaError {
654 #[error("schema already exists")]
659 AlreadyExists,
660 #[error("registry not found")]
663 RegistryNotFound,
664 #[error("AWS Glue error: {0}")]
667 Other(String),
668}
669
670fn classify_create_schema_error(err: SdkError<SdkCreateSchemaError>) -> CreateSchemaError {
671 if let SdkError::ServiceError(service_err) = &err {
672 match service_err.err() {
673 SdkCreateSchemaError::AlreadyExistsException(_) => {
674 return CreateSchemaError::AlreadyExists;
675 }
676 SdkCreateSchemaError::EntityNotFoundException(_) => {
677 return CreateSchemaError::RegistryNotFound;
678 }
679 _ => {}
680 }
681 }
682 CreateSchemaError::Other(DisplayErrorContext(&err).to_string())
683}
684
685#[derive(Debug, Error)]
687pub enum GetSchemaError {
688 #[error("schema not found")]
690 NotFound,
691 #[error("AWS Glue error: {0}")]
693 Other(String),
694}
695
696fn classify_get_schema_error(err: SdkError<SdkGetSchemaError>) -> GetSchemaError {
697 if let SdkError::ServiceError(service_err) = &err
698 && matches!(
699 service_err.err(),
700 SdkGetSchemaError::EntityNotFoundException(_)
701 )
702 {
703 return GetSchemaError::NotFound;
704 }
705 GetSchemaError::Other(DisplayErrorContext(&err).to_string())
706}
707
708#[cfg(test)]
709mod tests {
710 use aws_sdk_glue::operation::create_schema::CreateSchemaOutput;
711 use aws_sdk_glue::operation::get_schema_by_definition::GetSchemaByDefinitionOutput;
712 use aws_sdk_glue::operation::register_schema_version::RegisterSchemaVersionOutput;
713 use aws_smithy_mocks::{RuleMode, mock, mock_client};
714
715 use super::*;
716
717 const VERSION_ID: &str = "12345678-1234-5678-1234-567812345678";
718
719 #[mz_ore::test(tokio::test)]
723 async fn write_methods_surface_lifecycle_status() {
724 let register = mock!(aws_sdk_glue::Client::register_schema_version).then_output(|| {
725 RegisterSchemaVersionOutput::builder()
726 .schema_version_id(VERSION_ID)
727 .status(SdkSchemaVersionStatus::Pending)
728 .build()
729 });
730 let by_definition =
731 mock!(aws_sdk_glue::Client::get_schema_by_definition).then_output(|| {
732 GetSchemaByDefinitionOutput::builder()
733 .schema_version_id(VERSION_ID)
734 .status(SdkSchemaVersionStatus::Failure)
735 .build()
736 });
737 let create = mock!(aws_sdk_glue::Client::create_schema).then_output(|| {
738 CreateSchemaOutput::builder()
739 .schema_version_id(VERSION_ID)
740 .schema_version_status(SdkSchemaVersionStatus::Available)
741 .build()
742 });
743 let client = Client {
744 inner: mock_client!(
745 aws_sdk_glue,
746 RuleMode::MatchAny,
747 &[®ister, &by_definition, &create]
748 ),
749 };
750
751 let id = Uuid::parse_str(VERSION_ID).expect("valid uuid literal");
752 assert_eq!(
753 client
754 .register_schema_version("registry", "schema", "{}")
755 .await
756 .expect("mocked register succeeds"),
757 RegisteredSchemaVersion {
758 id,
759 lifecycle_status: Some(SchemaVersionLifecycleStatus::Pending),
760 }
761 );
762 assert_eq!(
763 client
764 .get_schema_by_definition("registry", "schema", "{}")
765 .await
766 .expect("mocked lookup succeeds"),
767 RegisteredSchemaVersion {
768 id,
769 lifecycle_status: Some(SchemaVersionLifecycleStatus::Failure),
770 }
771 );
772 assert_eq!(
773 client
774 .create_schema(
775 "registry",
776 "schema",
777 DataFormat::Avro,
778 Compatibility::Backward,
779 "{}",
780 )
781 .await
782 .expect("mocked create succeeds"),
783 RegisteredSchemaVersion {
784 id,
785 lifecycle_status: Some(SchemaVersionLifecycleStatus::Available),
786 }
787 );
788 }
789
790 #[mz_ore::test]
791 fn parse_schema_version_id_valid() {
792 let uuid = Uuid::parse_str("12345678-1234-5678-1234-567812345678").unwrap();
793 assert_eq!(parse_schema_version_id(Some(uuid.to_string())), Ok(uuid));
794 }
795
796 #[mz_ore::test]
797 fn parse_schema_version_id_missing() {
798 let err = parse_schema_version_id(None).unwrap_err();
799 assert!(err.contains("missing schema version id"), "{err}");
800 }
801
802 #[mz_ore::test]
803 fn parse_schema_version_id_malformed() {
804 let err = parse_schema_version_id(Some("not-a-uuid".to_string())).unwrap_err();
805 assert!(err.contains("invalid Glue schema version id"), "{err}");
806 }
807
808 #[mz_ore::test]
809 fn compatibility_sdk_round_trip() {
810 for c in [
813 Compatibility::None,
814 Compatibility::Disabled,
815 Compatibility::Backward,
816 Compatibility::BackwardAll,
817 Compatibility::Forward,
818 Compatibility::ForwardAll,
819 Compatibility::Full,
820 Compatibility::FullAll,
821 ] {
822 assert_eq!(Compatibility::from_sdk(c.to_sdk()), c);
823 }
824 }
825
826 #[mz_ore::test]
827 fn data_format_sdk_round_trip() {
828 for f in [DataFormat::Avro, DataFormat::Json, DataFormat::Protobuf] {
829 assert_eq!(DataFormat::from_sdk(f.to_sdk()), f);
830 }
831 }
832}