Skip to main content

hydracache_client_protocol/
java_migration.rs

1//! Java/Spring migration contract for legacy Hazelcast consumers.
2//!
3//! The real Java artifacts live outside the Cargo workspace, but their public
4//! behavior is anchored here so the Rust protocol gate can enforce stable error
5//! mapping, safe codec registration, and fail-loud unsupported Hazelcast APIs.
6
7use std::collections::BTreeMap;
8
9use thiserror::Error;
10
11use crate::{ClientErrorCode, ClientErrorEnvelope};
12
13/// Current Java migration contract version.
14pub const JAVA_MIGRATION_CONTRACT_VERSION: u16 = 2;
15
16/// Checked-in manifest of Hazelcast APIs that HydraCache refuses to emulate.
17pub const UNSUPPORTED_HAZELCAST_APIS_MANIFEST: &str =
18    include_str!("../manifests/unsupported_hazelcast_apis.txt");
19
20/// Supported Spring Boot generations for the 0.49 Java migration contract.
21pub const SUPPORTED_SPRING_BOOT_GENERATIONS: &[u8] = &[2, 3, 4];
22
23/// Java exception kind documented for the Java client and Spring toolkit.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum JavaExceptionKind {
26    /// No common protocol or an unsupported protocol feature was used.
27    Protocol,
28    /// The request did not carry an accepted identity.
29    Authentication,
30    /// The identity is known but not allowed to perform the operation.
31    Authorization,
32    /// Tenant quota was exceeded.
33    QuotaExceeded,
34    /// The caller is over its rate or fairness budget.
35    RateLimited,
36    /// Data-residency governance refused the operation.
37    ResidencyDenied,
38    /// Request frame or value bytes exceeded a configured bound.
39    PayloadTooLarge,
40    /// Request deadline expired.
41    Timeout,
42    /// Conditional or optimistic operation conflicted.
43    Conflict,
44    /// The backend is temporarily unavailable.
45    BackendUnavailable,
46    /// The client sent a malformed binary frame.
47    MalformedFrame,
48}
49
50impl JavaExceptionKind {
51    /// Stable Java class name for this exception kind.
52    pub const fn class_name(self) -> &'static str {
53        match self {
54            Self::Protocol => "HydraCacheProtocolException",
55            Self::Authentication => "HydraCacheAuthenticationException",
56            Self::Authorization => "HydraCacheAuthorizationException",
57            Self::QuotaExceeded => "HydraCacheQuotaExceededException",
58            Self::RateLimited => "HydraCacheRateLimitedException",
59            Self::ResidencyDenied => "HydraCacheResidencyDeniedException",
60            Self::PayloadTooLarge => "HydraCachePayloadTooLargeException",
61            Self::Timeout => "HydraCacheTimeoutException",
62            Self::Conflict => "HydraCacheConflictException",
63            Self::BackendUnavailable => "HydraCacheBackendUnavailableException",
64            Self::MalformedFrame => "HydraCacheMalformedFrameException",
65        }
66    }
67}
68
69/// Java-facing mapping for one stable protocol error.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct JavaExceptionMapping {
72    /// Documented exception kind.
73    pub kind: JavaExceptionKind,
74    /// Java class name the SDK must expose.
75    pub class_name: &'static str,
76    /// Retryability copied from the protocol envelope.
77    pub retryable: bool,
78    /// Whether the Java exception must retain the request id.
79    pub preserves_request_id: bool,
80    /// Whether the Java exception must retain retry-after metadata when present.
81    pub preserves_retry_after: bool,
82}
83
84/// Return the Java exception mapping for a stable protocol error.
85pub fn java_exception_mapping(error: &ClientErrorEnvelope) -> JavaExceptionMapping {
86    let kind = match error.code {
87        ClientErrorCode::IncompatibleVersion => JavaExceptionKind::Protocol,
88        ClientErrorCode::Unauthenticated => JavaExceptionKind::Authentication,
89        ClientErrorCode::Unauthorized => JavaExceptionKind::Authorization,
90        ClientErrorCode::TenantQuota => JavaExceptionKind::QuotaExceeded,
91        ClientErrorCode::RateLimited => JavaExceptionKind::RateLimited,
92        ClientErrorCode::ResidencyDenied => JavaExceptionKind::ResidencyDenied,
93        ClientErrorCode::TooLarge => JavaExceptionKind::PayloadTooLarge,
94        ClientErrorCode::DeadlineExceeded => JavaExceptionKind::Timeout,
95        ClientErrorCode::Conflict => JavaExceptionKind::Conflict,
96        ClientErrorCode::BackendUnavailable => JavaExceptionKind::BackendUnavailable,
97        ClientErrorCode::MalformedFrame => JavaExceptionKind::MalformedFrame,
98    };
99
100    JavaExceptionMapping {
101        kind,
102        class_name: kind.class_name(),
103        retryable: error.retryable,
104        preserves_request_id: true,
105        preserves_retry_after: error.retry_after_ms.is_some(),
106    }
107}
108
109/// Java application topology mode for migration from Hazelcast.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum JavaClientTopology {
112    /// Application JVM connects as a client.
113    Client,
114    /// Application JVM tries to become a data-owning member.
115    Member,
116    /// Toolkit is disabled and does not create a client.
117    None,
118}
119
120/// Public identity source for Java clients.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum JavaClientIdentityMode {
123    /// Bearer/API token supplied by the application.
124    Token,
125    /// Client certificate identity over mTLS.
126    Mtls,
127}
128
129/// Retry/backoff defaults for Java clients.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct JavaRetryBackoff {
132    /// Maximum attempts including the original call.
133    pub max_attempts: u8,
134    /// Initial backoff in milliseconds.
135    pub initial_backoff_ms: u64,
136    /// Maximum backoff in milliseconds.
137    pub max_backoff_ms: u64,
138}
139
140impl Default for JavaRetryBackoff {
141    fn default() -> Self {
142        Self {
143            max_attempts: 3,
144            initial_backoff_ms: 25,
145            max_backoff_ms: 1_000,
146        }
147    }
148}
149
150/// Java client runtime settings shared by the Boot 2/3/4 starters.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct JavaClientRuntimeConfig {
153    /// Client endpoints.
154    pub endpoints: Vec<String>,
155    /// Tenant id carried to W4 isolation.
156    pub tenant: String,
157    /// Caller-visible client name.
158    pub client_name: String,
159    /// Whether the Java client may use server-provided routing hints.
160    pub smart_routing: bool,
161    /// Identity source.
162    pub identity: JavaClientIdentityMode,
163    /// Application topology mode.
164    pub topology: JavaClientTopology,
165    /// Retry/backoff policy.
166    pub retry: JavaRetryBackoff,
167    /// Default request deadline in milliseconds.
168    pub deadline_ms: u64,
169    /// Whether customizers are allowed to mutate the transport settings.
170    pub customizer_hooks_enabled: bool,
171}
172
173impl JavaClientRuntimeConfig {
174    /// Build client-first defaults.
175    pub fn client_first(
176        endpoints: impl IntoIterator<Item = impl Into<String>>,
177        tenant: impl Into<String>,
178        client_name: impl Into<String>,
179    ) -> Self {
180        Self {
181            endpoints: endpoints.into_iter().map(Into::into).collect(),
182            tenant: tenant.into(),
183            client_name: client_name.into(),
184            smart_routing: true,
185            identity: JavaClientIdentityMode::Token,
186            topology: JavaClientTopology::Client,
187            retry: JavaRetryBackoff::default(),
188            deadline_ms: 5_000,
189            customizer_hooks_enabled: true,
190        }
191    }
192
193    /// Validate release-0.49 Java client defaults.
194    pub fn validate(&self) -> Result<(), JavaMigrationContractError> {
195        if self.endpoints.is_empty() {
196            return Err(JavaMigrationContractError::InvalidField("endpoints"));
197        }
198        if self.tenant.trim().is_empty() {
199            return Err(JavaMigrationContractError::InvalidField("tenant"));
200        }
201        if self.client_name.trim().is_empty() {
202            return Err(JavaMigrationContractError::InvalidField("client_name"));
203        }
204        if self.deadline_ms == 0 {
205            return Err(JavaMigrationContractError::InvalidField("deadline_ms"));
206        }
207        if self.retry.max_attempts == 0 {
208            return Err(JavaMigrationContractError::InvalidField(
209                "retry.max_attempts",
210            ));
211        }
212        if self.topology == JavaClientTopology::Member {
213            return Err(JavaMigrationContractError::UnsupportedClientTopology(
214                "member",
215            ));
216        }
217        Ok(())
218    }
219}
220
221/// Spring Cache integration mode.
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum SpringCacheMode {
224    /// Lazy map-backed cache names for legacy `CacheUtil` style code.
225    Native,
226    /// Bind to JCache when the optional JCache provider is present.
227    JCache,
228    /// Do not create a Spring `CacheManager`.
229    None,
230}
231
232impl SpringCacheMode {
233    /// Return whether this mode must lazily resolve dynamic cache names.
234    pub const fn lazy_dynamic_cache_names(self) -> bool {
235        matches!(self, Self::Native)
236    }
237
238    /// Return whether this mode requires a JCache provider.
239    pub const fn requires_jcache_provider(self) -> bool {
240        matches!(self, Self::JCache)
241    }
242}
243
244/// Java map facade operation exposed by the migration toolkit.
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum JavaMapOperation {
247    /// `HydraCacheMap.get`.
248    Get,
249    /// `HydraCacheMap.put`.
250    Put,
251    /// `HydraCacheMap.putIfAbsent`.
252    PutIfAbsent,
253    /// `HydraCacheMap.replace(key, oldValue, newValue)`.
254    Replace,
255    /// `HydraCacheMap.replace(key, newValue)`.
256    ReplaceIfPresent,
257    /// `HydraCacheMap.remove`.
258    Remove,
259    /// `HydraCacheMap.remove(key, value)`.
260    RemoveIfValue,
261    /// `HydraCacheMap.addEntryListener`.
262    AddEntryListener,
263    /// `HydraCacheMap.containsKey`.
264    ContainsKey,
265    /// `HydraCacheMap.getAll`.
266    GetAll,
267    /// `HydraCacheMap.putAll`.
268    PutAll,
269    /// Key invalidation.
270    Invalidate,
271    /// Namespace clear for the map.
272    ClearNamespace,
273    /// Region/namespace eviction.
274    EvictRegion,
275}
276
277/// Protocol-level operation family for a Java map facade method.
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum JavaMapProtocolFamily {
280    /// Maps to protocol-v1 get.
281    Get,
282    /// Maps to protocol-v1 put.
283    Put,
284    /// Maps to protocol-v1 conflict-aware conditional put-if-absent.
285    ConditionalPutIfAbsent,
286    /// Maps to protocol-v2 compare-and-set replace.
287    ConditionalReplace {
288        /// Which wire expectation shape the facade must use.
289        expectation: JavaMapCasExpectation,
290    },
291    /// Maps to protocol-v2 conditional tombstone.
292    ConditionalRemove,
293    /// Maps to protocol subscription with an entry-event projection.
294    SubscribeInvalidations {
295        /// Whether the mapping requests IMap entry-event shaping.
296        projection: JavaMapListenerProjection,
297    },
298    /// Maps to protocol-v1 invalidation.
299    Invalidate,
300    /// Maps to protocol-v1 batch get.
301    BatchGet,
302    /// Maps to protocol-v1 batch put.
303    BatchPut,
304    /// Maps to protocol-v1 namespace/region eviction.
305    EvictRegion,
306}
307
308/// Java map CAS expectation shape.
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum JavaMapCasExpectation {
311    /// The caller provides an exact old value.
312    ExactValue,
313    /// Any live value is acceptable, but absent/tombstoned keys mismatch.
314    Present,
315}
316
317/// Java listener projection over HydraCache invalidation signals.
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum JavaMapListenerProjection {
320    /// IMap entry-event shaped cache signal.
321    EntryEvent,
322}
323
324impl JavaMapOperation {
325    /// Return the protocol family that backs this facade operation.
326    pub const fn protocol_family(self) -> JavaMapProtocolFamily {
327        match self {
328            Self::Get | Self::ContainsKey => JavaMapProtocolFamily::Get,
329            Self::Put => JavaMapProtocolFamily::Put,
330            Self::PutIfAbsent => JavaMapProtocolFamily::ConditionalPutIfAbsent,
331            Self::Replace => JavaMapProtocolFamily::ConditionalReplace {
332                expectation: JavaMapCasExpectation::ExactValue,
333            },
334            Self::ReplaceIfPresent => JavaMapProtocolFamily::ConditionalReplace {
335                expectation: JavaMapCasExpectation::Present,
336            },
337            Self::RemoveIfValue => JavaMapProtocolFamily::ConditionalRemove,
338            Self::AddEntryListener => JavaMapProtocolFamily::SubscribeInvalidations {
339                projection: JavaMapListenerProjection::EntryEvent,
340            },
341            Self::Remove | Self::Invalidate => JavaMapProtocolFamily::Invalidate,
342            Self::GetAll => JavaMapProtocolFamily::BatchGet,
343            Self::PutAll => JavaMapProtocolFamily::BatchPut,
344            Self::ClearNamespace | Self::EvictRegion => JavaMapProtocolFamily::EvictRegion,
345        }
346    }
347}
348
349/// Java lock facade operation exposed by the migration toolkit.
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
351pub enum JavaLockOperation {
352    /// `Lock.lock`.
353    Lock,
354    /// Hazelcast CP `lockAndGetFence`.
355    LockAndGetFence,
356    /// Immediate `tryLock`.
357    TryLock,
358    /// Timed `tryLock`.
359    TryLockTimed,
360    /// Explicit `unlock`.
361    Unlock,
362    /// Read the current fencing token.
363    GetFence,
364    /// Read whether the lock is held.
365    IsLocked,
366    /// Check whether this session/endpoint owns the lock.
367    IsLockedByCurrentThread,
368    /// Privileged force unlock.
369    ForceUnlock,
370}
371
372/// Protocol-level operation family for a Java lock facade method.
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum JavaLockProtocolFamily {
375    /// Maps to protocol-v2 `TryLock` and may wait/retry client-side.
376    TryLock {
377        /// Whether the Java facade returns the fence directly.
378        returns_fence: bool,
379        /// Whether the facade models blocking wait semantics.
380        blocking_wait: bool,
381    },
382    /// Maps to protocol-v2 `Unlock`.
383    Unlock,
384    /// Maps to protocol-v2 `GetLockOwnership`.
385    GetLockOwnership,
386    /// Maps to protocol-v2 privileged `ForceUnlock`.
387    ForceUnlock {
388        /// Force unlock always requires admin authorization and audit.
389        privileged: bool,
390    },
391}
392
393impl JavaLockOperation {
394    /// Return the protocol family that backs this facade operation.
395    pub const fn protocol_family(self) -> JavaLockProtocolFamily {
396        match self {
397            Self::Lock => JavaLockProtocolFamily::TryLock {
398                returns_fence: false,
399                blocking_wait: true,
400            },
401            Self::LockAndGetFence => JavaLockProtocolFamily::TryLock {
402                returns_fence: true,
403                blocking_wait: true,
404            },
405            Self::TryLock => JavaLockProtocolFamily::TryLock {
406                returns_fence: true,
407                blocking_wait: false,
408            },
409            Self::TryLockTimed => JavaLockProtocolFamily::TryLock {
410                returns_fence: true,
411                blocking_wait: true,
412            },
413            Self::Unlock => JavaLockProtocolFamily::Unlock,
414            Self::GetFence | Self::IsLocked | Self::IsLockedByCurrentThread => {
415                JavaLockProtocolFamily::GetLockOwnership
416            }
417            Self::ForceUnlock => JavaLockProtocolFamily::ForceUnlock { privileged: true },
418        }
419    }
420
421    /// Return whether this operation requires admin authorization and audit.
422    pub const fn is_privileged(self) -> bool {
423        matches!(self, Self::ForceUnlock)
424    }
425}
426
427/// Serializer registration kind accepted or rejected by the Java toolkit.
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429pub enum JavaCodecKind {
430    /// Explicit codec instance or generated schema.
431    Explicit,
432    /// Package-scanned `@HydraCacheCodec`.
433    CodecAnnotation,
434    /// Package-scanned `@HydraCacheSchema`.
435    SchemaAnnotation,
436    /// Legacy serializer bridge enabled for migration only.
437    LegacySerializerBridge,
438    /// Reflective fallback serializer.
439    ReflectiveFallback,
440    /// Java native serialization.
441    JavaNativeSerialization,
442}
443
444impl JavaCodecKind {
445    /// Stable config label.
446    pub const fn as_str(self) -> &'static str {
447        match self {
448            Self::Explicit => "explicit",
449            Self::CodecAnnotation => "codec-annotation",
450            Self::SchemaAnnotation => "schema-annotation",
451            Self::LegacySerializerBridge => "legacy-serializer-bridge",
452            Self::ReflectiveFallback => "reflective-fallback",
453            Self::JavaNativeSerialization => "java-native-serialization",
454        }
455    }
456}
457
458/// Codec/schema descriptor registered by the Java migration toolkit.
459#[derive(Debug, Clone, PartialEq, Eq)]
460pub struct JavaCodecDescriptor {
461    /// Stable codec id.
462    pub codec_id: String,
463    /// Fully qualified Java type name.
464    pub java_type: String,
465    /// Schema version for reviewable migrations.
466    pub schema_version: u32,
467    /// Registration kind.
468    pub kind: JavaCodecKind,
469}
470
471impl JavaCodecDescriptor {
472    /// Create a descriptor.
473    pub fn new(
474        codec_id: impl Into<String>,
475        java_type: impl Into<String>,
476        schema_version: u32,
477        kind: JavaCodecKind,
478    ) -> Result<Self, JavaMigrationContractError> {
479        let descriptor = Self {
480            codec_id: codec_id.into(),
481            java_type: java_type.into(),
482            schema_version,
483            kind,
484        };
485        descriptor.validate()?;
486        Ok(descriptor)
487    }
488
489    /// Create an explicit codec descriptor.
490    pub fn explicit(
491        codec_id: impl Into<String>,
492        java_type: impl Into<String>,
493        schema_version: u32,
494    ) -> Result<Self, JavaMigrationContractError> {
495        Self::new(codec_id, java_type, schema_version, JavaCodecKind::Explicit)
496    }
497
498    fn validate(&self) -> Result<(), JavaMigrationContractError> {
499        if self.codec_id.trim().is_empty() {
500            return Err(JavaMigrationContractError::InvalidField("codec_id"));
501        }
502        if self.java_type.trim().is_empty() {
503            return Err(JavaMigrationContractError::InvalidField("java_type"));
504        }
505        if self.schema_version == 0 {
506            return Err(JavaMigrationContractError::InvalidField("schema_version"));
507        }
508        Ok(())
509    }
510}
511
512/// Safe codec registry contract for Java clients.
513#[derive(Debug, Clone, Default, PartialEq, Eq)]
514pub struct JavaCodecRegistryContract {
515    codecs: BTreeMap<String, JavaCodecDescriptor>,
516    legacy_serializer_bridge_enabled: bool,
517}
518
519impl JavaCodecRegistryContract {
520    /// Create a registry with safe defaults.
521    pub fn new() -> Self {
522        Self::default()
523    }
524
525    /// Explicitly allow the migration-only legacy serializer bridge.
526    pub fn with_legacy_serializer_bridge_enabled(mut self) -> Self {
527        self.legacy_serializer_bridge_enabled = true;
528        self
529    }
530
531    /// Register a descriptor, failing loud on ambiguous or unsafe serializers.
532    pub fn register(
533        &mut self,
534        descriptor: JavaCodecDescriptor,
535    ) -> Result<(), JavaMigrationContractError> {
536        match descriptor.kind {
537            JavaCodecKind::ReflectiveFallback | JavaCodecKind::JavaNativeSerialization => {
538                return Err(JavaMigrationContractError::UnsupportedSerializer(
539                    descriptor.kind.as_str(),
540                ));
541            }
542            JavaCodecKind::LegacySerializerBridge if !self.legacy_serializer_bridge_enabled => {
543                return Err(JavaMigrationContractError::LegacySerializerBridgeDisabled(
544                    descriptor.codec_id,
545                ));
546            }
547            _ => {}
548        }
549
550        if self.codecs.contains_key(&descriptor.codec_id) {
551            return Err(JavaMigrationContractError::AmbiguousCodec {
552                codec_id: descriptor.codec_id,
553            });
554        }
555
556        self.codecs.insert(descriptor.codec_id.clone(), descriptor);
557        Ok(())
558    }
559
560    /// Return a descriptor by codec id.
561    pub fn get(&self, codec_id: &str) -> Option<&JavaCodecDescriptor> {
562        self.codecs.get(codec_id)
563    }
564
565    /// Number of registered descriptors.
566    pub fn len(&self) -> usize {
567        self.codecs.len()
568    }
569
570    /// Return whether the registry is empty.
571    pub fn is_empty(&self) -> bool {
572        self.codecs.is_empty()
573    }
574}
575
576/// One unsupported Hazelcast API and its migration hint.
577#[derive(Debug, Clone, PartialEq, Eq)]
578pub struct UnsupportedHazelcastApi {
579    /// Hazelcast API surface.
580    pub api: String,
581    /// Human migration hint.
582    pub migration_hint: String,
583}
584
585/// One supported Hazelcast API migration mapping and its caveat.
586#[derive(Debug, Clone, PartialEq, Eq)]
587pub struct SupportedHazelcastApiMapping {
588    /// Hazelcast API surface.
589    pub api: String,
590    /// Human migration hint.
591    pub migration_hint: String,
592}
593
594/// Versioned manifest of supported mappings and unsupported Hazelcast APIs.
595#[derive(Debug, Clone, PartialEq, Eq)]
596pub struct UnsupportedHazelcastApiManifest {
597    /// Manifest version.
598    pub version: u16,
599    /// Unsupported APIs.
600    pub entries: Vec<UnsupportedHazelcastApi>,
601    /// Supported source-level migration mappings.
602    pub supported_mappings: Vec<SupportedHazelcastApiMapping>,
603}
604
605impl UnsupportedHazelcastApiManifest {
606    /// Parse a manifest and reject unknown future versions.
607    pub fn parse(contents: &str) -> Result<Self, JavaMigrationContractError> {
608        let mut version = None;
609        let mut entries = Vec::new();
610        let mut supported_mappings = Vec::new();
611
612        for raw in contents.lines() {
613            let line = raw.trim();
614            if line.is_empty() || line.starts_with('#') {
615                continue;
616            }
617
618            if let Some(value) = line.strip_prefix("version=") {
619                let parsed = value
620                    .parse()
621                    .map_err(|_| JavaMigrationContractError::InvalidManifest("version"))?;
622                version = Some(parsed);
623                continue;
624            }
625
626            let parts = line.split('|').map(str::trim).collect::<Vec<_>>();
627            let (kind, api, migration_hint) = match parts.as_slice() {
628                [api, migration_hint] => ("unsupported", *api, *migration_hint),
629                [kind @ ("unsupported" | "supported"), api, migration_hint] => {
630                    (*kind, *api, *migration_hint)
631                }
632                _ => {
633                    return Err(JavaMigrationContractError::InvalidManifest("entry"));
634                }
635            };
636            if api.is_empty() || migration_hint.is_empty() {
637                return Err(JavaMigrationContractError::InvalidManifest("entry"));
638            }
639            match kind {
640                "unsupported" => entries.push(UnsupportedHazelcastApi {
641                    api: api.to_owned(),
642                    migration_hint: migration_hint.to_owned(),
643                }),
644                "supported" => supported_mappings.push(SupportedHazelcastApiMapping {
645                    api: api.to_owned(),
646                    migration_hint: migration_hint.to_owned(),
647                }),
648                _ => return Err(JavaMigrationContractError::InvalidManifest("entry")),
649            }
650        }
651
652        let version = version.ok_or(JavaMigrationContractError::InvalidManifest("version"))?;
653        if version != JAVA_MIGRATION_CONTRACT_VERSION {
654            return Err(JavaMigrationContractError::UnsupportedManifestVersion {
655                actual: version,
656                supported: JAVA_MIGRATION_CONTRACT_VERSION,
657            });
658        }
659        if entries.is_empty() {
660            return Err(JavaMigrationContractError::InvalidManifest("entries"));
661        }
662
663        Ok(Self {
664            version,
665            entries,
666            supported_mappings,
667        })
668    }
669
670    /// Parse the checked-in manifest.
671    pub fn checked_in() -> Result<Self, JavaMigrationContractError> {
672        Self::parse(UNSUPPORTED_HAZELCAST_APIS_MANIFEST)
673    }
674
675    /// Find a manifest entry by API name.
676    pub fn find(&self, api: &str) -> Option<&UnsupportedHazelcastApi> {
677        self.entries.iter().find(|entry| entry.api == api)
678    }
679
680    /// Find a supported mapping entry by API name.
681    pub fn find_supported(&self, api: &str) -> Option<&SupportedHazelcastApiMapping> {
682        self.supported_mappings
683            .iter()
684            .find(|entry| entry.api == api)
685    }
686}
687
688/// Java migration contract errors.
689#[derive(Debug, Clone, PartialEq, Eq, Error)]
690pub enum JavaMigrationContractError {
691    /// A field is missing or invalid.
692    #[error("invalid java migration contract field: {0}")]
693    InvalidField(&'static str),
694    /// Application JVM member mode is unsupported in release 0.49.
695    #[error("unsupported java client topology for release 0.49: {0}")]
696    UnsupportedClientTopology(&'static str),
697    /// Codec id is ambiguous.
698    #[error("ambiguous java codec id: {codec_id}")]
699    AmbiguousCodec {
700        /// Duplicate codec id.
701        codec_id: String,
702    },
703    /// Serializer kind is unsupported.
704    #[error("unsupported java serializer kind: {0}")]
705    UnsupportedSerializer(&'static str),
706    /// Legacy serializer bridge is disabled by default.
707    #[error("legacy serializer bridge is disabled for codec id: {0}")]
708    LegacySerializerBridgeDisabled(String),
709    /// Manifest is malformed.
710    #[error("invalid unsupported Hazelcast API manifest: {0}")]
711    InvalidManifest(&'static str),
712    /// Manifest version is newer than this library supports.
713    #[error(
714        "unsupported unsupported-Hazelcast-API manifest version {actual}; supported {supported}"
715    )]
716    UnsupportedManifestVersion {
717        /// Actual manifest version.
718        actual: u16,
719        /// Supported manifest version.
720        supported: u16,
721    },
722}