1use chrono::{DateTime, Utc};
26use serde::{Deserialize, Serialize};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum System {
31 Gingr,
33 BusinessIntelligence,
35 LaborScheduling,
37 Timeclock,
39 Payroll,
41 CapacityInventory,
43 PointOfSale,
45 ManualImport,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
50pub struct Timestamp(DateTime<Utc>);
52
53impl Timestamp {
54 pub fn try_new(value: impl AsRef<str>) -> Result<Self> {
56 let value = value.as_ref().trim();
57 if value.is_empty() {
58 return Err(Error::EmptyTimestamp);
59 }
60 let parsed = value
61 .parse::<DateTime<Utc>>()
62 .map_err(|_| Error::InvalidTimestamp)?;
63 Ok(Self(parsed))
64 }
65
66 pub const fn get(&self) -> &DateTime<Utc> {
68 &self.0
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
73pub struct Endpoint(String);
75
76impl Endpoint {
77 pub fn try_new(value: impl Into<String>) -> Result<Self> {
79 trimmed_non_empty(value, Error::EmptyEndpoint).map(Self)
80 }
81
82 pub fn as_str(&self) -> &str {
84 &self.0
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
89pub struct ExtractionBatchId(String);
91
92impl ExtractionBatchId {
93 pub fn try_new(value: impl Into<String>) -> Result<Self> {
95 trimmed_non_empty(value, Error::EmptyExtractionBatch).map(Self)
96 }
97
98 pub fn as_str(&self) -> &str {
100 &self.0
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
105pub struct RequestScope(String);
107
108impl RequestScope {
109 pub fn try_new(value: impl Into<String>) -> Result<Self> {
111 trimmed_non_empty(value, Error::EmptyRequestScope).map(Self)
112 }
113
114 pub fn as_str(&self) -> &str {
116 &self.0
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
121pub struct SchemaVersion(String);
123
124impl SchemaVersion {
125 pub fn try_new(value: impl Into<String>) -> Result<Self> {
127 trimmed_non_empty(value, Error::EmptySchemaVersion).map(Self)
128 }
129
130 pub fn as_str(&self) -> &str {
132 &self.0
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
137pub struct PayloadHash(String);
139
140impl PayloadHash {
141 pub fn try_new(value: impl Into<String>) -> Result<Self> {
143 trimmed_non_empty(value, Error::EmptyPayloadHash).map(Self)
144 }
145
146 pub fn as_str(&self) -> &str {
148 &self.0
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
153pub struct RawPayloadRef(String);
155
156impl RawPayloadRef {
157 pub fn try_new(value: impl Into<String>) -> Result<Self> {
159 trimmed_non_empty(value, Error::EmptyRawPayloadRef).map(Self)
160 }
161
162 pub fn as_str(&self) -> &str {
164 &self.0
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
169pub struct ObservedStatus(String);
171
172impl ObservedStatus {
173 pub fn try_new(value: impl Into<String>) -> Result<Self> {
175 trimmed_non_empty(value, Error::EmptyObservedStatus).map(Self)
176 }
177
178 pub fn as_str(&self) -> &str {
180 &self.0
181 }
182}
183
184pub mod record {
186 use serde::{Deserialize, Serialize};
187
188 use crate::source;
189
190 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
191 pub struct Id(String);
193
194 impl Id {
195 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
197 source::trimmed_non_empty(value, source::Error::EmptyRecordId).map(Self)
198 }
199
200 pub fn as_str(&self) -> &str {
202 &self.0
203 }
204 }
205
206 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
207 pub enum Role {
209 Customer,
211 Pet,
213 Location,
215 ReservationType,
217 Invoice,
219 Payment,
221 Service,
223 Staff,
225 Unknown,
227 }
228
229 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230 pub struct RelatedId {
232 role: Role,
233 id: Id,
234 }
235
236 impl RelatedId {
237 pub const fn new(role: Role, id: Id) -> Self {
239 Self { role, id }
240 }
241
242 pub const fn role(&self) -> Role {
244 self.role
245 }
246
247 pub const fn id(&self) -> &Id {
249 &self.id
250 }
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct RecordRef {
257 system: System,
258 record_id: record::Id,
259}
260
261impl RecordRef {
262 pub const fn new(system: System, record_id: record::Id) -> Self {
264 Self { system, record_id }
265 }
266
267 pub fn from_provenance(provenance: &Provenance) -> Self {
269 Self::new(provenance.system(), provenance.record_id().clone())
270 }
271
272 pub const fn system(&self) -> System {
274 self.system
275 }
276
277 pub const fn record_id(&self) -> &record::Id {
279 &self.record_id
280 }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
284pub struct Provenance {
286 system: System,
287 endpoint: Endpoint,
288 record_id: record::Id,
289 #[builder(default)]
290 related_record_ids: Vec<record::RelatedId>,
291 extraction_batch: ExtractionBatchId,
292 pulled_at: Timestamp,
293 request_scope: RequestScope,
294 schema_version: SchemaVersion,
295 payload_hash: PayloadHash,
296 raw_payload_ref: RawPayloadRef,
297}
298
299impl Provenance {
300 pub const fn system(&self) -> System {
302 self.system
303 }
304
305 pub const fn source_system(&self) -> System {
307 self.system
308 }
309
310 pub const fn endpoint(&self) -> &Endpoint {
312 &self.endpoint
313 }
314
315 pub const fn record_id(&self) -> &record::Id {
317 &self.record_id
318 }
319
320 pub fn related_record_ids(&self) -> &[record::RelatedId] {
322 &self.related_record_ids
323 }
324
325 pub const fn extraction_batch(&self) -> &ExtractionBatchId {
327 &self.extraction_batch
328 }
329
330 pub const fn pulled_at(&self) -> &Timestamp {
332 &self.pulled_at
333 }
334
335 pub const fn request_scope(&self) -> &RequestScope {
337 &self.request_scope
338 }
339
340 pub const fn schema_version(&self) -> &SchemaVersion {
342 &self.schema_version
343 }
344
345 pub const fn payload_hash(&self) -> &PayloadHash {
347 &self.payload_hash
348 }
349
350 pub const fn raw_payload_ref(&self) -> &RawPayloadRef {
352 &self.raw_payload_ref
353 }
354}
355
356pub mod reservation {
358 use serde::{Deserialize, Serialize};
359
360 use crate::{data_quality, source};
361
362 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
363 pub enum OwnerPetRelationship {
365 Resolved,
367 Ambiguous {
369 candidate_count: u16,
371 },
372 }
373
374 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375 pub enum Status {
377 Requested,
379 Confirmed,
381 CheckedIn,
383 CheckedOut,
385 Cancelled,
387 Unknown {
389 observed: source::ObservedStatus,
391 },
392 }
393
394 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
395 pub enum Assumption {
397 GrainTreatedAsReservation,
399 CustomerRecordIdTreatedAsStableJoinKey,
401 PetRecordIdTreatedAsStableJoinKey,
403 ProviderStatusMappingIsProvisional,
405 RawPayloadRetentionUnknown,
407 RefreshMutationPolicyUnknown,
409 }
410
411 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
412 pub struct Snapshot {
414 provenance: source::Provenance,
415 customer_record_id: Option<source::record::Id>,
416 pet_record_id: Option<source::record::Id>,
417 location_record_id: Option<source::record::Id>,
418 service_type_record_id: Option<source::record::Id>,
419 status: Option<Status>,
420 relationship: OwnerPetRelationship,
421 assumptions: Vec<Assumption>,
422 }
423
424 impl Snapshot {
425 pub const fn builder() -> SnapshotBuilder {
427 SnapshotBuilder::new()
428 }
429
430 pub const fn provenance(&self) -> &source::Provenance {
432 &self.provenance
433 }
434
435 pub const fn customer_record_id(&self) -> Option<&source::record::Id> {
437 self.customer_record_id.as_ref()
438 }
439
440 pub const fn pet_record_id(&self) -> Option<&source::record::Id> {
442 self.pet_record_id.as_ref()
443 }
444
445 pub const fn location_record_id(&self) -> Option<&source::record::Id> {
447 self.location_record_id.as_ref()
448 }
449
450 pub const fn service_type_record_id(&self) -> Option<&source::record::Id> {
452 self.service_type_record_id.as_ref()
453 }
454
455 pub fn status(&self) -> Option<Status> {
457 self.status.clone()
458 }
459
460 pub const fn relationship(&self) -> &OwnerPetRelationship {
462 &self.relationship
463 }
464
465 pub fn assumptions(&self) -> &[Assumption] {
467 &self.assumptions
468 }
469
470 pub fn data_quality_issues(
472 &self,
473 detected_at: source::Timestamp,
474 ) -> Vec<data_quality::Issue> {
475 let mut issues = Vec::new();
476 self.push_missing_issue(
477 &mut issues,
478 self.customer_record_id.is_none(),
479 data_quality::FieldPath::reservation(
480 data_quality::ReservationField::CustomerRecordId,
481 ),
482 detected_at.clone(),
483 );
484 self.push_missing_issue(
485 &mut issues,
486 self.pet_record_id.is_none(),
487 data_quality::FieldPath::reservation(data_quality::ReservationField::PetRecordId),
488 detected_at.clone(),
489 );
490 self.push_missing_issue(
491 &mut issues,
492 self.location_record_id.is_none(),
493 data_quality::FieldPath::reservation(
494 data_quality::ReservationField::LocationRecordId,
495 ),
496 detected_at.clone(),
497 );
498 self.push_missing_issue(
499 &mut issues,
500 self.service_type_record_id.is_none(),
501 data_quality::FieldPath::reservation(
502 data_quality::ReservationField::ServiceTypeRecordId,
503 ),
504 detected_at.clone(),
505 );
506 if self.status.is_none() {
507 self.push_missing_issue(
508 &mut issues,
509 true,
510 data_quality::FieldPath::reservation(data_quality::ReservationField::Status),
511 detected_at.clone(),
512 );
513 issues.push(data_quality::Issue::new(
514 data_quality::Kind::AssumptionInForce {
515 assumption: Assumption::RefreshMutationPolicyUnknown,
516 },
517 data_quality::Severity::Blocking,
518 self.provenance.clone(),
519 detected_at.clone(),
520 true,
521 ));
522 }
523 if let Some(Status::Unknown { observed }) = &self.status {
524 issues.push(data_quality::Issue::new(
525 data_quality::Kind::UnknownSourceStatus {
526 observed: observed.clone(),
527 },
528 data_quality::Severity::Blocking,
529 self.provenance.clone(),
530 detected_at.clone(),
531 true,
532 ));
533 }
534 if matches!(self.relationship, OwnerPetRelationship::Ambiguous { .. }) {
535 issues.push(data_quality::Issue::new(
536 data_quality::Kind::AmbiguousOwnerPetRelationship,
537 data_quality::Severity::Blocking,
538 self.provenance.clone(),
539 detected_at.clone(),
540 true,
541 ));
542 }
543 for assumption in &self.assumptions {
544 if matches!(
545 assumption,
546 Assumption::RawPayloadRetentionUnknown
547 | Assumption::RefreshMutationPolicyUnknown
548 ) {
549 issues.push(data_quality::Issue::new(
550 data_quality::Kind::AssumptionInForce {
551 assumption: *assumption,
552 },
553 data_quality::Severity::Warning,
554 self.provenance.clone(),
555 detected_at.clone(),
556 false,
557 ));
558 }
559 }
560 issues
561 }
562
563 fn push_missing_issue(
564 &self,
565 issues: &mut Vec<data_quality::Issue>,
566 missing: bool,
567 field: data_quality::FieldPath,
568 detected_at: source::Timestamp,
569 ) {
570 if missing {
571 issues.push(data_quality::Issue::new(
572 data_quality::Kind::MissingRequiredField { field },
573 data_quality::Severity::Blocking,
574 self.provenance.clone(),
575 detected_at,
576 true,
577 ));
578 }
579 }
580 }
581
582 #[derive(Debug, Clone)]
583 pub struct SnapshotBuilder {
585 provenance: Option<source::Provenance>,
586 customer_record_id: Option<source::record::Id>,
587 pet_record_id: Option<source::record::Id>,
588 location_record_id: Option<source::record::Id>,
589 service_type_record_id: Option<source::record::Id>,
590 status: Option<Status>,
591 relationship: Option<OwnerPetRelationship>,
592 assumptions: Vec<Assumption>,
593 }
594
595 impl Default for SnapshotBuilder {
596 fn default() -> Self {
597 Self::new()
598 }
599 }
600
601 impl SnapshotBuilder {
602 pub const fn new() -> Self {
604 Self {
605 provenance: None,
606 customer_record_id: None,
607 pet_record_id: None,
608 location_record_id: None,
609 service_type_record_id: None,
610 status: None,
611 relationship: None,
612 assumptions: Vec::new(),
613 }
614 }
615
616 pub fn provenance(mut self, provenance: source::Provenance) -> Self {
618 self.provenance = Some(provenance);
619 self
620 }
621
622 pub fn customer_record_id(mut self, id: impl Into<Option<source::record::Id>>) -> Self {
624 self.customer_record_id = id.into();
625 self
626 }
627
628 pub fn pet_record_id(mut self, id: impl Into<Option<source::record::Id>>) -> Self {
630 self.pet_record_id = id.into();
631 self
632 }
633
634 pub fn location_record_id(mut self, id: impl Into<Option<source::record::Id>>) -> Self {
636 self.location_record_id = id.into();
637 self
638 }
639
640 pub fn service_type_record_id(mut self, id: impl Into<Option<source::record::Id>>) -> Self {
642 self.service_type_record_id = id.into();
643 self
644 }
645
646 pub fn status(mut self, status: impl Into<Option<Status>>) -> Self {
648 self.status = status.into();
649 self
650 }
651
652 pub fn relationship(mut self, relationship: OwnerPetRelationship) -> Self {
654 self.relationship = Some(relationship);
655 self
656 }
657
658 pub fn assumptions(mut self, assumptions: Vec<Assumption>) -> Self {
660 self.assumptions = assumptions;
661 self
662 }
663
664 pub fn build(self) -> Snapshot {
666 Snapshot {
667 provenance: self.provenance.expect("snapshot provenance is required"),
668 customer_record_id: self.customer_record_id,
669 pet_record_id: self.pet_record_id,
670 location_record_id: self.location_record_id,
671 service_type_record_id: self.service_type_record_id,
672 status: self.status,
673 relationship: self
674 .relationship
675 .expect("snapshot relationship is required"),
676 assumptions: self.assumptions,
677 }
678 }
679 }
680}
681
682pub mod gingr {
684 use bon::Builder;
685 use serde::{Deserialize, Serialize};
686
687 use crate::source;
688
689 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
690 pub struct Endpoint(String);
692
693 impl Endpoint {
694 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
696 source::trimmed_non_empty(value, source::Error::EmptyGingrEndpoint).map(Self)
697 }
698
699 pub fn as_str(&self) -> &str {
701 &self.0
702 }
703 }
704
705 impl From<Endpoint> for source::Endpoint {
706 fn from(value: Endpoint) -> Self {
707 source::Endpoint::try_new(value.0).expect("Gingr endpoint was already validated")
708 }
709 }
710
711 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
712 pub struct ProviderRecordId(String);
714
715 impl ProviderRecordId {
716 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
718 source::trimmed_non_empty(value, source::Error::EmptyProviderRecordId).map(Self)
719 }
720
721 pub fn as_str(&self) -> &str {
723 &self.0
724 }
725 }
726
727 impl From<ProviderRecordId> for source::record::Id {
728 fn from(value: ProviderRecordId) -> Self {
729 source::record::Id::try_new(value.0).expect("Gingr provider id was already validated")
730 }
731 }
732
733 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
734 pub enum RelatedProviderId {
736 Owner(ProviderRecordId),
738 Animal(ProviderRecordId),
740 Location(ProviderRecordId),
742 ReservationType(ProviderRecordId),
744 Invoice(ProviderRecordId),
746 Payment(ProviderRecordId),
748 Service(ProviderRecordId),
750 }
751
752 impl RelatedProviderId {
753 pub const fn owner(id: ProviderRecordId) -> Self {
755 Self::Owner(id)
756 }
757
758 pub const fn animal(id: ProviderRecordId) -> Self {
760 Self::Animal(id)
761 }
762
763 fn promote(self) -> source::record::RelatedId {
764 match self {
765 Self::Owner(id) => {
766 source::record::RelatedId::new(source::record::Role::Customer, id.into())
767 }
768 Self::Animal(id) => {
769 source::record::RelatedId::new(source::record::Role::Pet, id.into())
770 }
771 Self::Location(id) => {
772 source::record::RelatedId::new(source::record::Role::Location, id.into())
773 }
774 Self::ReservationType(id) => {
775 source::record::RelatedId::new(source::record::Role::ReservationType, id.into())
776 }
777 Self::Invoice(id) => {
778 source::record::RelatedId::new(source::record::Role::Invoice, id.into())
779 }
780 Self::Payment(id) => {
781 source::record::RelatedId::new(source::record::Role::Payment, id.into())
782 }
783 Self::Service(id) => {
784 source::record::RelatedId::new(source::record::Role::Service, id.into())
785 }
786 }
787 }
788 }
789
790 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
791 pub struct ExtractionBatchId(String);
793
794 impl ExtractionBatchId {
795 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
797 source::trimmed_non_empty(value, source::Error::EmptyExtractionBatch).map(Self)
798 }
799
800 pub fn as_str(&self) -> &str {
802 &self.0
803 }
804 }
805
806 impl From<ExtractionBatchId> for source::ExtractionBatchId {
807 fn from(value: ExtractionBatchId) -> Self {
808 source::ExtractionBatchId::try_new(value.0)
809 .expect("Gingr extraction batch id was already validated")
810 }
811 }
812
813 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
814 pub struct RequestScope(String);
816
817 impl RequestScope {
818 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
820 source::trimmed_non_empty(value, source::Error::EmptyRequestScope).map(Self)
821 }
822
823 pub fn as_str(&self) -> &str {
825 &self.0
826 }
827 }
828
829 impl From<RequestScope> for source::RequestScope {
830 fn from(value: RequestScope) -> Self {
831 source::RequestScope::try_new(value.0)
832 .expect("Gingr request scope was already validated")
833 }
834 }
835
836 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
837 pub struct ProviderSchemaVersion(String);
839
840 impl ProviderSchemaVersion {
841 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
843 source::trimmed_non_empty(value, source::Error::EmptyProviderSchemaVersion).map(Self)
844 }
845
846 pub fn as_str(&self) -> &str {
848 &self.0
849 }
850 }
851
852 impl From<ProviderSchemaVersion> for source::SchemaVersion {
853 fn from(value: ProviderSchemaVersion) -> Self {
854 source::SchemaVersion::try_new(value.0)
855 .expect("Gingr provider schema version was already validated")
856 }
857 }
858
859 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
860 pub struct ProviderStatus(String);
862
863 impl ProviderStatus {
864 pub fn try_new(value: impl Into<String>) -> source::Result<Self> {
866 source::trimmed_non_empty(value, source::Error::EmptyProviderStatus).map(Self)
867 }
868
869 pub fn as_str(&self) -> &str {
871 &self.0
872 }
873
874 fn promote(self) -> source::reservation::Status {
875 match self.0.trim().to_ascii_lowercase().as_str() {
876 "requested" | "request" | "pending" => source::reservation::Status::Requested,
877 "confirmed" | "booked" => source::reservation::Status::Confirmed,
878 "checked_in" | "checked-in" | "in_house" => source::reservation::Status::CheckedIn,
879 "checked_out" | "checked-out" | "complete" => {
880 source::reservation::Status::CheckedOut
881 }
882 "cancelled" | "canceled" => source::reservation::Status::Cancelled,
883 _ => source::reservation::Status::Unknown {
884 observed: source::ObservedStatus::try_new(self.0)
885 .expect("Gingr provider status was already validated"),
886 },
887 }
888 }
889 }
890
891 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
892 pub struct Provenance {
894 endpoint: Endpoint,
895 provider_record_id: ProviderRecordId,
896 #[builder(default)]
897 related_provider_ids: Vec<RelatedProviderId>,
898 extraction_batch: ExtractionBatchId,
899 pulled_at: source::Timestamp,
900 request_scope: RequestScope,
901 provider_schema_version: ProviderSchemaVersion,
902 source_payload_hash: source::PayloadHash,
903 raw_payload_ref: source::RawPayloadRef,
904 }
905
906 impl Provenance {
907 pub const fn source_system(&self) -> source::System {
909 source::System::Gingr
910 }
911
912 pub const fn endpoint(&self) -> &Endpoint {
914 &self.endpoint
915 }
916
917 pub const fn provider_record_id(&self) -> &ProviderRecordId {
919 &self.provider_record_id
920 }
921
922 pub fn related_provider_ids(&self) -> &[RelatedProviderId] {
924 &self.related_provider_ids
925 }
926
927 pub const fn extraction_batch(&self) -> &ExtractionBatchId {
929 &self.extraction_batch
930 }
931
932 pub const fn pulled_at(&self) -> &source::Timestamp {
934 &self.pulled_at
935 }
936
937 pub const fn raw_payload_ref(&self) -> &source::RawPayloadRef {
939 &self.raw_payload_ref
940 }
941
942 pub fn promote(self) -> source::Provenance {
944 source::Provenance::builder()
945 .system(source::System::Gingr)
946 .endpoint(self.endpoint.into())
947 .record_id(self.provider_record_id.into())
948 .related_record_ids(
949 self.related_provider_ids
950 .into_iter()
951 .map(RelatedProviderId::promote)
952 .collect(),
953 )
954 .extraction_batch(self.extraction_batch.into())
955 .pulled_at(self.pulled_at)
956 .request_scope(self.request_scope.into())
957 .schema_version(self.provider_schema_version.into())
958 .payload_hash(self.source_payload_hash)
959 .raw_payload_ref(self.raw_payload_ref)
960 .build()
961 }
962 }
963
964 pub mod reservation {
966 use serde::{Deserialize, Serialize};
967
968 use super::{Provenance, ProviderRecordId, ProviderStatus};
969 use crate::source;
970
971 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
972 pub enum OwnerPetRelationship {
974 Resolved,
976 Ambiguous {
978 candidate_count: u16,
980 },
981 }
982
983 impl From<OwnerPetRelationship> for source::reservation::OwnerPetRelationship {
984 fn from(value: OwnerPetRelationship) -> Self {
985 match value {
986 OwnerPetRelationship::Resolved => Self::Resolved,
987 OwnerPetRelationship::Ambiguous { candidate_count } => {
988 Self::Ambiguous { candidate_count }
989 }
990 }
991 }
992 }
993
994 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
995 pub struct Snapshot {
997 provenance: Provenance,
998 owner_provider_id: Option<ProviderRecordId>,
999 animal_provider_id: Option<ProviderRecordId>,
1000 location_provider_id: Option<ProviderRecordId>,
1001 service_type_provider_id: Option<ProviderRecordId>,
1002 provider_status: Option<ProviderStatus>,
1003 relationship: OwnerPetRelationship,
1004 }
1005
1006 impl Snapshot {
1007 pub const fn builder() -> SnapshotBuilder {
1009 SnapshotBuilder::new()
1010 }
1011
1012 pub const fn provenance(&self) -> &Provenance {
1014 &self.provenance
1015 }
1016
1017 pub const fn owner_provider_id(&self) -> Option<&ProviderRecordId> {
1019 self.owner_provider_id.as_ref()
1020 }
1021
1022 pub const fn animal_provider_id(&self) -> Option<&ProviderRecordId> {
1024 self.animal_provider_id.as_ref()
1025 }
1026
1027 pub const fn location_provider_id(&self) -> Option<&ProviderRecordId> {
1029 self.location_provider_id.as_ref()
1030 }
1031
1032 pub const fn service_type_provider_id(&self) -> Option<&ProviderRecordId> {
1034 self.service_type_provider_id.as_ref()
1035 }
1036
1037 pub const fn provider_status(&self) -> Option<&ProviderStatus> {
1039 self.provider_status.as_ref()
1040 }
1041
1042 pub const fn relationship(&self) -> &OwnerPetRelationship {
1044 &self.relationship
1045 }
1046
1047 pub fn promote(self) -> source::reservation::Snapshot {
1049 let status = self.provider_status.map(ProviderStatus::promote);
1050 let mut assumptions = vec![
1051 source::reservation::Assumption::GrainTreatedAsReservation,
1052 source::reservation::Assumption::CustomerRecordIdTreatedAsStableJoinKey,
1053 source::reservation::Assumption::PetRecordIdTreatedAsStableJoinKey,
1054 source::reservation::Assumption::ProviderStatusMappingIsProvisional,
1055 ];
1056 if status.is_none() {
1057 assumptions.push(source::reservation::Assumption::RefreshMutationPolicyUnknown);
1058 }
1059
1060 source::reservation::Snapshot::builder()
1061 .provenance(self.provenance.promote())
1062 .customer_record_id(self.owner_provider_id.map(Into::into))
1063 .pet_record_id(self.animal_provider_id.map(Into::into))
1064 .location_record_id(self.location_provider_id.map(Into::into))
1065 .service_type_record_id(self.service_type_provider_id.map(Into::into))
1066 .status(status)
1067 .relationship(self.relationship.into())
1068 .assumptions(assumptions)
1069 .build()
1070 }
1071 }
1072
1073 #[derive(Debug, Clone)]
1074 pub struct SnapshotBuilder {
1076 provenance: Option<Provenance>,
1077 owner_provider_id: Option<ProviderRecordId>,
1078 animal_provider_id: Option<ProviderRecordId>,
1079 location_provider_id: Option<ProviderRecordId>,
1080 service_type_provider_id: Option<ProviderRecordId>,
1081 provider_status: Option<ProviderStatus>,
1082 relationship: Option<OwnerPetRelationship>,
1083 }
1084
1085 impl Default for SnapshotBuilder {
1086 fn default() -> Self {
1087 Self::new()
1088 }
1089 }
1090
1091 impl SnapshotBuilder {
1092 pub const fn new() -> Self {
1094 Self {
1095 provenance: None,
1096 owner_provider_id: None,
1097 animal_provider_id: None,
1098 location_provider_id: None,
1099 service_type_provider_id: None,
1100 provider_status: None,
1101 relationship: None,
1102 }
1103 }
1104
1105 pub fn provenance(mut self, provenance: Provenance) -> Self {
1107 self.provenance = Some(provenance);
1108 self
1109 }
1110
1111 pub fn owner_provider_id(mut self, id: impl Into<Option<ProviderRecordId>>) -> Self {
1113 self.owner_provider_id = id.into();
1114 self
1115 }
1116
1117 pub fn animal_provider_id(mut self, id: impl Into<Option<ProviderRecordId>>) -> Self {
1119 self.animal_provider_id = id.into();
1120 self
1121 }
1122
1123 pub fn location_provider_id(mut self, id: impl Into<Option<ProviderRecordId>>) -> Self {
1125 self.location_provider_id = id.into();
1126 self
1127 }
1128
1129 pub fn service_type_provider_id(
1131 mut self,
1132 id: impl Into<Option<ProviderRecordId>>,
1133 ) -> Self {
1134 self.service_type_provider_id = id.into();
1135 self
1136 }
1137
1138 pub fn provider_status(mut self, status: impl Into<Option<ProviderStatus>>) -> Self {
1140 self.provider_status = status.into();
1141 self
1142 }
1143
1144 pub fn relationship(mut self, relationship: OwnerPetRelationship) -> Self {
1146 self.relationship = Some(relationship);
1147 self
1148 }
1149
1150 pub fn build(self) -> Snapshot {
1152 Snapshot {
1153 provenance: self.provenance.expect("snapshot provenance is required"),
1154 owner_provider_id: self.owner_provider_id,
1155 animal_provider_id: self.animal_provider_id,
1156 location_provider_id: self.location_provider_id,
1157 service_type_provider_id: self.service_type_provider_id,
1158 provider_status: self.provider_status,
1159 relationship: self
1160 .relationship
1161 .expect("snapshot relationship is required"),
1162 }
1163 }
1164 }
1165 }
1166}
1167
1168#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
1169pub enum Error {
1171 #[error("timestamp must not be empty")]
1172 EmptyTimestamp,
1174 #[error("timestamp must be RFC3339 UTC-compatible text")]
1175 InvalidTimestamp,
1177 #[error("source endpoint must not be empty")]
1178 EmptyEndpoint,
1180 #[error("Gingr endpoint must not be empty")]
1181 EmptyGingrEndpoint,
1183 #[error("source record id must not be empty")]
1184 EmptyRecordId,
1186 #[error("provider record id must not be empty")]
1187 EmptyProviderRecordId,
1189 #[error("extraction batch id must not be empty")]
1190 EmptyExtractionBatch,
1192 #[error("request scope must not be empty")]
1193 EmptyRequestScope,
1195 #[error("schema version must not be empty")]
1196 EmptySchemaVersion,
1198 #[error("provider schema version must not be empty")]
1199 EmptyProviderSchemaVersion,
1201 #[error("source payload hash must not be empty")]
1202 EmptyPayloadHash,
1204 #[error("raw payload reference must not be empty")]
1205 EmptyRawPayloadRef,
1207 #[error("observed status must not be empty")]
1208 EmptyObservedStatus,
1210 #[error("provider status must not be empty")]
1211 EmptyProviderStatus,
1213}
1214
1215pub type Result<T> = std::result::Result<T, Error>;
1217
1218fn trimmed_non_empty(value: impl Into<String>, empty_error: Error) -> Result<String> {
1219 let value = value.into().trim().to_string();
1220 if value.is_empty() {
1221 return Err(empty_error);
1222 }
1223 Ok(value)
1224}