1use chrono::{DateTime, Utc};
33use serde::{Deserialize, Serialize};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36pub enum System {
38 Gingr,
40 BusinessIntelligence,
42 LaborScheduling,
44 Timeclock,
46 Payroll,
48 CapacityInventory,
50 PointOfSale,
52 ManualImport,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
57pub struct Timestamp(DateTime<Utc>);
59
60impl Timestamp {
61 pub fn try_new(value: impl AsRef<str>) -> Result<Self> {
63 let value = value.as_ref().trim();
64 if value.is_empty() {
65 return Err(Error::EmptyTimestamp);
66 }
67 let parsed = value
68 .parse::<DateTime<Utc>>()
69 .map_err(|_| Error::InvalidTimestamp)?;
70 Ok(Self(parsed))
71 }
72
73 pub const fn get(&self) -> &DateTime<Utc> {
75 &self.0
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
80pub struct Endpoint(String);
82
83impl Endpoint {
84 pub fn try_new(value: impl Into<String>) -> Result<Self> {
86 trimmed_non_empty(value, Error::EmptyEndpoint).map(Self)
87 }
88
89 pub fn as_str(&self) -> &str {
91 &self.0
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
96pub struct ExtractionBatchId(String);
98
99impl ExtractionBatchId {
100 pub fn try_new(value: impl Into<String>) -> Result<Self> {
102 trimmed_non_empty(value, Error::EmptyExtractionBatch).map(Self)
103 }
104
105 pub fn as_str(&self) -> &str {
107 &self.0
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
112pub struct RequestScope(String);
114
115impl RequestScope {
116 pub fn try_new(value: impl Into<String>) -> Result<Self> {
118 trimmed_non_empty(value, Error::EmptyRequestScope).map(Self)
119 }
120
121 pub fn as_str(&self) -> &str {
123 &self.0
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
128pub struct SchemaVersion(String);
130
131impl SchemaVersion {
132 pub fn try_new(value: impl Into<String>) -> Result<Self> {
134 trimmed_non_empty(value, Error::EmptySchemaVersion).map(Self)
135 }
136
137 pub fn as_str(&self) -> &str {
139 &self.0
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
144pub struct PayloadHash(String);
146
147impl PayloadHash {
148 pub fn try_new(value: impl Into<String>) -> Result<Self> {
150 trimmed_non_empty(value, Error::EmptyPayloadHash).map(Self)
151 }
152
153 pub fn as_str(&self) -> &str {
155 &self.0
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
160pub struct RawPayloadRef(String);
162
163impl RawPayloadRef {
164 pub fn try_new(value: impl Into<String>) -> Result<Self> {
166 trimmed_non_empty(value, Error::EmptyRawPayloadRef).map(Self)
167 }
168
169 pub fn as_str(&self) -> &str {
171 &self.0
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
176pub struct ObservedStatus(String);
178
179impl ObservedStatus {
180 pub fn try_new(value: impl Into<String>) -> Result<Self> {
182 trimmed_non_empty(value, Error::EmptyObservedStatus).map(Self)
183 }
184
185 pub fn as_str(&self) -> &str {
187 &self.0
188 }
189}
190
191pub mod record {
193 use serde::{Deserialize, Serialize};
194
195 use crate::source;
196
197 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
198 pub struct Id(String);
200
201 impl Id {
202 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
204 source::trimmed_non_empty(value, source::Error::EmptyRecordId).map(Self)
205 }
206
207 pub fn as_str(&self) -> &str {
209 &self.0
210 }
211 }
212
213 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
214 pub enum Role {
216 Customer,
218 Pet,
220 Location,
222 ReservationType,
224 Invoice,
226 Payment,
228 Service,
230 Staff,
232 Unknown,
234 }
235
236 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237 pub struct RelatedId {
239 role: Role,
240 id: Id,
241 }
242
243 impl RelatedId {
244 pub const fn new(role: Role, id: Id) -> Self {
246 Self { role, id }
247 }
248
249 pub const fn role(&self) -> Role {
251 self.role
252 }
253
254 pub const fn id(&self) -> &Id {
256 &self.id
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262pub struct RecordRef {
264 system: System,
265 record_id: record::Id,
266}
267
268impl RecordRef {
269 pub const fn new(system: System, record_id: record::Id) -> Self {
271 Self { system, record_id }
272 }
273
274 pub fn from_provenance(provenance: &Provenance) -> Self {
276 Self::new(provenance.system(), provenance.record_id().clone())
277 }
278
279 pub const fn system(&self) -> System {
281 self.system
282 }
283
284 pub const fn record_id(&self) -> &record::Id {
286 &self.record_id
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
291pub struct Provenance {
293 system: System,
294 endpoint: Endpoint,
295 record_id: record::Id,
296 #[builder(default)]
297 related_record_ids: Vec<record::RelatedId>,
298 extraction_batch: ExtractionBatchId,
299 pulled_at: Timestamp,
300 request_scope: RequestScope,
301 schema_version: SchemaVersion,
302 payload_hash: PayloadHash,
303 raw_payload_ref: RawPayloadRef,
304}
305
306impl Provenance {
307 pub const fn system(&self) -> System {
309 self.system
310 }
311
312 pub const fn source_system(&self) -> System {
314 self.system
315 }
316
317 pub const fn endpoint(&self) -> &Endpoint {
319 &self.endpoint
320 }
321
322 pub const fn record_id(&self) -> &record::Id {
324 &self.record_id
325 }
326
327 pub fn related_record_ids(&self) -> &[record::RelatedId] {
329 &self.related_record_ids
330 }
331
332 pub const fn extraction_batch(&self) -> &ExtractionBatchId {
334 &self.extraction_batch
335 }
336
337 pub const fn pulled_at(&self) -> &Timestamp {
339 &self.pulled_at
340 }
341
342 pub const fn request_scope(&self) -> &RequestScope {
344 &self.request_scope
345 }
346
347 pub const fn schema_version(&self) -> &SchemaVersion {
349 &self.schema_version
350 }
351
352 pub const fn payload_hash(&self) -> &PayloadHash {
354 &self.payload_hash
355 }
356
357 pub const fn raw_payload_ref(&self) -> &RawPayloadRef {
359 &self.raw_payload_ref
360 }
361}
362
363pub mod reservation {
365 use serde::{Deserialize, Serialize};
366
367 use crate::{data_quality, source};
368
369 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
370 pub enum OwnerPetRelationship {
372 Resolved,
374 Ambiguous {
376 candidate_count: u16,
378 },
379 }
380
381 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382 pub enum Status {
384 Requested,
386 Confirmed,
388 CheckedIn,
390 CheckedOut,
392 Cancelled,
394 Unknown {
396 observed: source::ObservedStatus,
398 },
399 }
400
401 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
402 pub enum Assumption {
404 GrainTreatedAsReservation,
406 CustomerRecordIdTreatedAsStableJoinKey,
408 PetRecordIdTreatedAsStableJoinKey,
410 ProviderStatusMappingIsProvisional,
412 RawPayloadRetentionUnknown,
414 RefreshMutationPolicyUnknown,
416 }
417
418 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419 pub struct Snapshot {
421 provenance: source::Provenance,
422 customer_record_id: Option<source::record::Id>,
423 pet_record_id: Option<source::record::Id>,
424 location_record_id: Option<source::record::Id>,
425 service_type_record_id: Option<source::record::Id>,
426 status: Option<Status>,
427 relationship: OwnerPetRelationship,
428 assumptions: Vec<Assumption>,
429 }
430
431 impl Snapshot {
432 pub const fn builder() -> SnapshotBuilder {
434 SnapshotBuilder::new()
435 }
436
437 pub const fn provenance(&self) -> &source::Provenance {
439 &self.provenance
440 }
441
442 pub const fn customer_record_id(&self) -> Option<&source::record::Id> {
444 self.customer_record_id.as_ref()
445 }
446
447 pub const fn pet_record_id(&self) -> Option<&source::record::Id> {
449 self.pet_record_id.as_ref()
450 }
451
452 pub const fn location_record_id(&self) -> Option<&source::record::Id> {
454 self.location_record_id.as_ref()
455 }
456
457 pub const fn service_type_record_id(&self) -> Option<&source::record::Id> {
459 self.service_type_record_id.as_ref()
460 }
461
462 pub fn status(&self) -> Option<Status> {
464 self.status.clone()
465 }
466
467 pub const fn relationship(&self) -> &OwnerPetRelationship {
469 &self.relationship
470 }
471
472 pub fn assumptions(&self) -> &[Assumption] {
474 &self.assumptions
475 }
476
477 pub fn data_quality_issues(
479 &self,
480 detected_at: source::Timestamp,
481 ) -> Vec<data_quality::Issue> {
482 let mut issues = Vec::new();
483 self.push_missing_issue(
484 &mut issues,
485 self.customer_record_id.is_none(),
486 data_quality::FieldPath::reservation(
487 data_quality::ReservationField::CustomerRecordId,
488 ),
489 detected_at.clone(),
490 );
491 self.push_missing_issue(
492 &mut issues,
493 self.pet_record_id.is_none(),
494 data_quality::FieldPath::reservation(data_quality::ReservationField::PetRecordId),
495 detected_at.clone(),
496 );
497 self.push_missing_issue(
498 &mut issues,
499 self.location_record_id.is_none(),
500 data_quality::FieldPath::reservation(
501 data_quality::ReservationField::LocationRecordId,
502 ),
503 detected_at.clone(),
504 );
505 self.push_missing_issue(
506 &mut issues,
507 self.service_type_record_id.is_none(),
508 data_quality::FieldPath::reservation(
509 data_quality::ReservationField::ServiceTypeRecordId,
510 ),
511 detected_at.clone(),
512 );
513 if self.status.is_none() {
514 self.push_missing_issue(
515 &mut issues,
516 true,
517 data_quality::FieldPath::reservation(data_quality::ReservationField::Status),
518 detected_at.clone(),
519 );
520 issues.push(data_quality::Issue::new(
521 data_quality::Kind::AssumptionInForce {
522 assumption: Assumption::RefreshMutationPolicyUnknown,
523 },
524 data_quality::Severity::Blocking,
525 self.provenance.clone(),
526 detected_at.clone(),
527 true,
528 ));
529 }
530 if let Some(Status::Unknown { observed }) = &self.status {
531 issues.push(data_quality::Issue::new(
532 data_quality::Kind::UnknownSourceStatus {
533 observed: observed.clone(),
534 },
535 data_quality::Severity::Blocking,
536 self.provenance.clone(),
537 detected_at.clone(),
538 true,
539 ));
540 }
541 if matches!(self.relationship, OwnerPetRelationship::Ambiguous { .. }) {
542 issues.push(data_quality::Issue::new(
543 data_quality::Kind::AmbiguousOwnerPetRelationship,
544 data_quality::Severity::Blocking,
545 self.provenance.clone(),
546 detected_at.clone(),
547 true,
548 ));
549 }
550 for assumption in &self.assumptions {
551 if matches!(
552 assumption,
553 Assumption::RawPayloadRetentionUnknown
554 | Assumption::RefreshMutationPolicyUnknown
555 ) {
556 issues.push(data_quality::Issue::new(
557 data_quality::Kind::AssumptionInForce {
558 assumption: *assumption,
559 },
560 data_quality::Severity::Warning,
561 self.provenance.clone(),
562 detected_at.clone(),
563 false,
564 ));
565 }
566 }
567 issues
568 }
569
570 fn push_missing_issue(
571 &self,
572 issues: &mut Vec<data_quality::Issue>,
573 missing: bool,
574 field: data_quality::FieldPath,
575 detected_at: source::Timestamp,
576 ) {
577 if missing {
578 issues.push(data_quality::Issue::new(
579 data_quality::Kind::MissingRequiredField { field },
580 data_quality::Severity::Blocking,
581 self.provenance.clone(),
582 detected_at,
583 true,
584 ));
585 }
586 }
587 }
588
589 #[derive(Debug, Clone)]
590 pub struct SnapshotBuilder {
592 provenance: Option<source::Provenance>,
593 customer_record_id: Option<source::record::Id>,
594 pet_record_id: Option<source::record::Id>,
595 location_record_id: Option<source::record::Id>,
596 service_type_record_id: Option<source::record::Id>,
597 status: Option<Status>,
598 relationship: Option<OwnerPetRelationship>,
599 assumptions: Vec<Assumption>,
600 }
601
602 impl Default for SnapshotBuilder {
603 fn default() -> Self {
604 Self::new()
605 }
606 }
607
608 impl SnapshotBuilder {
609 pub const fn new() -> Self {
611 Self {
612 provenance: None,
613 customer_record_id: None,
614 pet_record_id: None,
615 location_record_id: None,
616 service_type_record_id: None,
617 status: None,
618 relationship: None,
619 assumptions: Vec::new(),
620 }
621 }
622
623 pub fn provenance(mut self, provenance: source::Provenance) -> Self {
625 self.provenance = Some(provenance);
626 self
627 }
628
629 pub fn customer_record_id(mut self, id: impl Into<Option<source::record::Id>>) -> Self {
631 self.customer_record_id = id.into();
632 self
633 }
634
635 pub fn pet_record_id(mut self, id: impl Into<Option<source::record::Id>>) -> Self {
637 self.pet_record_id = id.into();
638 self
639 }
640
641 pub fn location_record_id(mut self, id: impl Into<Option<source::record::Id>>) -> Self {
643 self.location_record_id = id.into();
644 self
645 }
646
647 pub fn service_type_record_id(mut self, id: impl Into<Option<source::record::Id>>) -> Self {
649 self.service_type_record_id = id.into();
650 self
651 }
652
653 pub fn status(mut self, status: impl Into<Option<Status>>) -> Self {
655 self.status = status.into();
656 self
657 }
658
659 pub fn relationship(mut self, relationship: OwnerPetRelationship) -> Self {
661 self.relationship = Some(relationship);
662 self
663 }
664
665 pub fn assumptions(mut self, assumptions: Vec<Assumption>) -> Self {
667 self.assumptions = assumptions;
668 self
669 }
670
671 pub fn build(self) -> Snapshot {
673 Snapshot {
674 provenance: self.provenance.expect("snapshot provenance is required"),
675 customer_record_id: self.customer_record_id,
676 pet_record_id: self.pet_record_id,
677 location_record_id: self.location_record_id,
678 service_type_record_id: self.service_type_record_id,
679 status: self.status,
680 relationship: self
681 .relationship
682 .expect("snapshot relationship is required"),
683 assumptions: self.assumptions,
684 }
685 }
686 }
687}
688
689pub mod gingr {
691 use bon::Builder;
692 use serde::{Deserialize, Serialize};
693
694 use crate::source;
695
696 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
697 pub struct Endpoint(String);
699
700 impl Endpoint {
701 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
703 source::trimmed_non_empty(value, source::Error::EmptyGingrEndpoint).map(Self)
704 }
705
706 pub fn as_str(&self) -> &str {
708 &self.0
709 }
710 }
711
712 impl From<Endpoint> for source::Endpoint {
713 fn from(value: Endpoint) -> Self {
714 source::Endpoint::try_new(value.0).expect("Gingr endpoint was already validated")
715 }
716 }
717
718 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
719 pub struct ProviderRecordId(String);
721
722 impl ProviderRecordId {
723 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
725 source::trimmed_non_empty(value, source::Error::EmptyProviderRecordId).map(Self)
726 }
727
728 pub fn as_str(&self) -> &str {
730 &self.0
731 }
732 }
733
734 impl From<ProviderRecordId> for source::record::Id {
735 fn from(value: ProviderRecordId) -> Self {
736 source::record::Id::try_new(value.0).expect("Gingr provider id was already validated")
737 }
738 }
739
740 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
741 pub enum RelatedProviderId {
743 Owner(ProviderRecordId),
745 Animal(ProviderRecordId),
747 Location(ProviderRecordId),
749 ReservationType(ProviderRecordId),
751 Invoice(ProviderRecordId),
753 Payment(ProviderRecordId),
755 Service(ProviderRecordId),
757 }
758
759 impl RelatedProviderId {
760 pub const fn owner(id: ProviderRecordId) -> Self {
762 Self::Owner(id)
763 }
764
765 pub const fn animal(id: ProviderRecordId) -> Self {
767 Self::Animal(id)
768 }
769
770 fn promote(self) -> source::record::RelatedId {
771 match self {
772 Self::Owner(id) => {
773 source::record::RelatedId::new(source::record::Role::Customer, id.into())
774 }
775 Self::Animal(id) => {
776 source::record::RelatedId::new(source::record::Role::Pet, id.into())
777 }
778 Self::Location(id) => {
779 source::record::RelatedId::new(source::record::Role::Location, id.into())
780 }
781 Self::ReservationType(id) => {
782 source::record::RelatedId::new(source::record::Role::ReservationType, id.into())
783 }
784 Self::Invoice(id) => {
785 source::record::RelatedId::new(source::record::Role::Invoice, id.into())
786 }
787 Self::Payment(id) => {
788 source::record::RelatedId::new(source::record::Role::Payment, id.into())
789 }
790 Self::Service(id) => {
791 source::record::RelatedId::new(source::record::Role::Service, id.into())
792 }
793 }
794 }
795 }
796
797 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
798 pub struct ExtractionBatchId(String);
800
801 impl ExtractionBatchId {
802 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
804 source::trimmed_non_empty(value, source::Error::EmptyExtractionBatch).map(Self)
805 }
806
807 pub fn as_str(&self) -> &str {
809 &self.0
810 }
811 }
812
813 impl From<ExtractionBatchId> for source::ExtractionBatchId {
814 fn from(value: ExtractionBatchId) -> Self {
815 source::ExtractionBatchId::try_new(value.0)
816 .expect("Gingr extraction batch id was already validated")
817 }
818 }
819
820 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
821 pub struct RequestScope(String);
823
824 impl RequestScope {
825 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
827 source::trimmed_non_empty(value, source::Error::EmptyRequestScope).map(Self)
828 }
829
830 pub fn as_str(&self) -> &str {
832 &self.0
833 }
834 }
835
836 impl From<RequestScope> for source::RequestScope {
837 fn from(value: RequestScope) -> Self {
838 source::RequestScope::try_new(value.0)
839 .expect("Gingr request scope was already validated")
840 }
841 }
842
843 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
844 pub struct ProviderSchemaVersion(String);
846
847 impl ProviderSchemaVersion {
848 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
850 source::trimmed_non_empty(value, source::Error::EmptyProviderSchemaVersion).map(Self)
851 }
852
853 pub fn as_str(&self) -> &str {
855 &self.0
856 }
857 }
858
859 impl From<ProviderSchemaVersion> for source::SchemaVersion {
860 fn from(value: ProviderSchemaVersion) -> Self {
861 source::SchemaVersion::try_new(value.0)
862 .expect("Gingr provider schema version was already validated")
863 }
864 }
865
866 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
867 pub struct ProviderStatus(String);
869
870 impl ProviderStatus {
871 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
873 source::trimmed_non_empty(value, source::Error::EmptyProviderStatus).map(Self)
874 }
875
876 pub fn as_str(&self) -> &str {
878 &self.0
879 }
880
881 fn promote(self) -> source::reservation::Status {
882 match self.0.trim().to_ascii_lowercase().as_str() {
883 "requested" | "request" | "pending" => source::reservation::Status::Requested,
884 "confirmed" | "booked" => source::reservation::Status::Confirmed,
885 "checked_in" | "checked-in" | "in_house" => source::reservation::Status::CheckedIn,
886 "checked_out" | "checked-out" | "complete" => {
887 source::reservation::Status::CheckedOut
888 }
889 "cancelled" | "canceled" => source::reservation::Status::Cancelled,
890 _ => source::reservation::Status::Unknown {
891 observed: source::ObservedStatus::try_new(self.0)
892 .expect("Gingr provider status was already validated"),
893 },
894 }
895 }
896 }
897
898 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
899 pub struct Provenance {
901 endpoint: Endpoint,
902 provider_record_id: ProviderRecordId,
903 #[builder(default)]
904 related_provider_ids: Vec<RelatedProviderId>,
905 extraction_batch: ExtractionBatchId,
906 pulled_at: source::Timestamp,
907 request_scope: RequestScope,
908 provider_schema_version: ProviderSchemaVersion,
909 source_payload_hash: source::PayloadHash,
910 raw_payload_ref: source::RawPayloadRef,
911 }
912
913 impl Provenance {
914 pub const fn source_system(&self) -> source::System {
916 source::System::Gingr
917 }
918
919 pub const fn endpoint(&self) -> &Endpoint {
921 &self.endpoint
922 }
923
924 pub const fn provider_record_id(&self) -> &ProviderRecordId {
926 &self.provider_record_id
927 }
928
929 pub fn related_provider_ids(&self) -> &[RelatedProviderId] {
931 &self.related_provider_ids
932 }
933
934 pub const fn extraction_batch(&self) -> &ExtractionBatchId {
936 &self.extraction_batch
937 }
938
939 pub const fn pulled_at(&self) -> &source::Timestamp {
941 &self.pulled_at
942 }
943
944 pub const fn raw_payload_ref(&self) -> &source::RawPayloadRef {
946 &self.raw_payload_ref
947 }
948
949 pub fn promote(self) -> source::Provenance {
951 source::Provenance::builder()
952 .system(source::System::Gingr)
953 .endpoint(self.endpoint.into())
954 .record_id(self.provider_record_id.into())
955 .related_record_ids(
956 self.related_provider_ids
957 .into_iter()
958 .map(RelatedProviderId::promote)
959 .collect(),
960 )
961 .extraction_batch(self.extraction_batch.into())
962 .pulled_at(self.pulled_at)
963 .request_scope(self.request_scope.into())
964 .schema_version(self.provider_schema_version.into())
965 .payload_hash(self.source_payload_hash)
966 .raw_payload_ref(self.raw_payload_ref)
967 .build()
968 }
969 }
970
971 pub mod reservation {
973 use serde::{Deserialize, Serialize};
974
975 use super::{Provenance, ProviderRecordId, ProviderStatus};
976 use crate::source;
977
978 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
979 pub enum OwnerPetRelationship {
981 Resolved,
983 Ambiguous {
985 candidate_count: u16,
987 },
988 }
989
990 impl From<OwnerPetRelationship> for source::reservation::OwnerPetRelationship {
991 fn from(value: OwnerPetRelationship) -> Self {
992 match value {
993 OwnerPetRelationship::Resolved => Self::Resolved,
994 OwnerPetRelationship::Ambiguous { candidate_count } => {
995 Self::Ambiguous { candidate_count }
996 }
997 }
998 }
999 }
1000
1001 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1002 pub struct Snapshot {
1004 provenance: Provenance,
1005 owner_provider_id: Option<ProviderRecordId>,
1006 animal_provider_id: Option<ProviderRecordId>,
1007 location_provider_id: Option<ProviderRecordId>,
1008 service_type_provider_id: Option<ProviderRecordId>,
1009 provider_status: Option<ProviderStatus>,
1010 relationship: OwnerPetRelationship,
1011 }
1012
1013 impl Snapshot {
1014 pub const fn builder() -> SnapshotBuilder {
1016 SnapshotBuilder::new()
1017 }
1018
1019 pub const fn provenance(&self) -> &Provenance {
1021 &self.provenance
1022 }
1023
1024 pub const fn owner_provider_id(&self) -> Option<&ProviderRecordId> {
1026 self.owner_provider_id.as_ref()
1027 }
1028
1029 pub const fn animal_provider_id(&self) -> Option<&ProviderRecordId> {
1031 self.animal_provider_id.as_ref()
1032 }
1033
1034 pub const fn location_provider_id(&self) -> Option<&ProviderRecordId> {
1036 self.location_provider_id.as_ref()
1037 }
1038
1039 pub const fn service_type_provider_id(&self) -> Option<&ProviderRecordId> {
1041 self.service_type_provider_id.as_ref()
1042 }
1043
1044 pub const fn provider_status(&self) -> Option<&ProviderStatus> {
1046 self.provider_status.as_ref()
1047 }
1048
1049 pub const fn relationship(&self) -> &OwnerPetRelationship {
1051 &self.relationship
1052 }
1053
1054 pub fn promote(self) -> source::reservation::Snapshot {
1056 let status = self.provider_status.map(ProviderStatus::promote);
1057 let mut assumptions = vec![
1058 source::reservation::Assumption::GrainTreatedAsReservation,
1059 source::reservation::Assumption::CustomerRecordIdTreatedAsStableJoinKey,
1060 source::reservation::Assumption::PetRecordIdTreatedAsStableJoinKey,
1061 source::reservation::Assumption::ProviderStatusMappingIsProvisional,
1062 ];
1063 if status.is_none() {
1064 assumptions.push(source::reservation::Assumption::RefreshMutationPolicyUnknown);
1065 }
1066
1067 source::reservation::Snapshot::builder()
1068 .provenance(self.provenance.promote())
1069 .customer_record_id(self.owner_provider_id.map(Into::into))
1070 .pet_record_id(self.animal_provider_id.map(Into::into))
1071 .location_record_id(self.location_provider_id.map(Into::into))
1072 .service_type_record_id(self.service_type_provider_id.map(Into::into))
1073 .status(status)
1074 .relationship(self.relationship.into())
1075 .assumptions(assumptions)
1076 .build()
1077 }
1078 }
1079
1080 #[derive(Debug, Clone)]
1081 pub struct SnapshotBuilder {
1083 provenance: Option<Provenance>,
1084 owner_provider_id: Option<ProviderRecordId>,
1085 animal_provider_id: Option<ProviderRecordId>,
1086 location_provider_id: Option<ProviderRecordId>,
1087 service_type_provider_id: Option<ProviderRecordId>,
1088 provider_status: Option<ProviderStatus>,
1089 relationship: Option<OwnerPetRelationship>,
1090 }
1091
1092 impl Default for SnapshotBuilder {
1093 fn default() -> Self {
1094 Self::new()
1095 }
1096 }
1097
1098 impl SnapshotBuilder {
1099 pub const fn new() -> Self {
1101 Self {
1102 provenance: None,
1103 owner_provider_id: None,
1104 animal_provider_id: None,
1105 location_provider_id: None,
1106 service_type_provider_id: None,
1107 provider_status: None,
1108 relationship: None,
1109 }
1110 }
1111
1112 pub fn provenance(mut self, provenance: Provenance) -> Self {
1114 self.provenance = Some(provenance);
1115 self
1116 }
1117
1118 pub fn owner_provider_id(mut self, id: impl Into<Option<ProviderRecordId>>) -> Self {
1120 self.owner_provider_id = id.into();
1121 self
1122 }
1123
1124 pub fn animal_provider_id(mut self, id: impl Into<Option<ProviderRecordId>>) -> Self {
1126 self.animal_provider_id = id.into();
1127 self
1128 }
1129
1130 pub fn location_provider_id(mut self, id: impl Into<Option<ProviderRecordId>>) -> Self {
1132 self.location_provider_id = id.into();
1133 self
1134 }
1135
1136 pub fn service_type_provider_id(
1138 mut self,
1139 id: impl Into<Option<ProviderRecordId>>,
1140 ) -> Self {
1141 self.service_type_provider_id = id.into();
1142 self
1143 }
1144
1145 pub fn provider_status(mut self, status: impl Into<Option<ProviderStatus>>) -> Self {
1147 self.provider_status = status.into();
1148 self
1149 }
1150
1151 pub fn relationship(mut self, relationship: OwnerPetRelationship) -> Self {
1153 self.relationship = Some(relationship);
1154 self
1155 }
1156
1157 pub fn build(self) -> Snapshot {
1159 Snapshot {
1160 provenance: self.provenance.expect("snapshot provenance is required"),
1161 owner_provider_id: self.owner_provider_id,
1162 animal_provider_id: self.animal_provider_id,
1163 location_provider_id: self.location_provider_id,
1164 service_type_provider_id: self.service_type_provider_id,
1165 provider_status: self.provider_status,
1166 relationship: self
1167 .relationship
1168 .expect("snapshot relationship is required"),
1169 }
1170 }
1171 }
1172 }
1173}
1174
1175#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
1176pub enum Error {
1178 #[error("timestamp must not be empty")]
1179 EmptyTimestamp,
1181 #[error("timestamp must be RFC3339 UTC-compatible text")]
1182 InvalidTimestamp,
1184 #[error("source endpoint must not be empty")]
1185 EmptyEndpoint,
1187 #[error("Gingr endpoint must not be empty")]
1188 EmptyGingrEndpoint,
1190 #[error("source record id must not be empty")]
1191 EmptyRecordId,
1193 #[error("provider record id must not be empty")]
1194 EmptyProviderRecordId,
1196 #[error("extraction batch id must not be empty")]
1197 EmptyExtractionBatch,
1199 #[error("request scope must not be empty")]
1200 EmptyRequestScope,
1202 #[error("schema version must not be empty")]
1203 EmptySchemaVersion,
1205 #[error("provider schema version must not be empty")]
1206 EmptyProviderSchemaVersion,
1208 #[error("source payload hash must not be empty")]
1209 EmptyPayloadHash,
1211 #[error("raw payload reference must not be empty")]
1212 EmptyRawPayloadRef,
1214 #[error("observed status must not be empty")]
1215 EmptyObservedStatus,
1217 #[error("provider status must not be empty")]
1218 EmptyProviderStatus,
1220}
1221
1222pub type Result<T> = std::result::Result<T, Error>;
1224
1225fn trimmed_non_empty(value: impl Into<String>, empty_error: Error) -> Result<String> {
1226 let value = value.into().trim().to_string();
1227 if value.is_empty() {
1228 return Err(empty_error);
1229 }
1230 Ok(value)
1231}