1use serde::{Deserialize, Serialize};
4
5use crate::util::validate_non_empty;
6use crate::{
7 GenericListingActorKind, GenericListingFreshnessState, GenericListingQuery,
8 GenericRegistryPublisher, SignedGenericListing, GENERIC_LISTING_SEARCH_ALGORITHM_V1,
9};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(rename_all = "camelCase")]
13pub struct GenericListingFreshnessWindow {
14 pub max_age_secs: u64,
15 pub valid_until: u64,
16}
17
18impl GenericListingFreshnessWindow {
19 pub fn validate(&self, generated_at: u64) -> Result<(), String> {
20 if self.max_age_secs == 0 {
21 return Err("freshness.max_age_secs must be greater than zero".to_string());
22 }
23 if self.valid_until <= generated_at {
24 return Err("freshness.valid_until must be greater than generated_at".to_string());
25 }
26 Ok(())
27 }
28
29 #[must_use]
30 pub fn assess(&self, generated_at: u64, now: u64) -> GenericListingReplicaFreshness {
31 let age_secs = now.saturating_sub(generated_at);
32 let state = if generated_at > now || age_secs > self.max_age_secs || now > self.valid_until
33 {
34 GenericListingFreshnessState::Stale
35 } else {
36 GenericListingFreshnessState::Fresh
37 };
38 GenericListingReplicaFreshness {
39 state,
40 age_secs,
41 max_age_secs: self.max_age_secs,
42 valid_until: self.valid_until,
43 generated_at,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49#[serde(rename_all = "camelCase")]
50pub struct GenericListingSearchPolicy {
51 pub algorithm: String,
52 pub reproducible_ordering: bool,
53 pub freshness_affects_ranking: bool,
54 pub visibility_only: bool,
55 pub explicit_trust_activation_required: bool,
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub ranking_inputs: Vec<String>,
58}
59
60impl Default for GenericListingSearchPolicy {
61 fn default() -> Self {
62 Self {
63 algorithm: GENERIC_LISTING_SEARCH_ALGORITHM_V1.to_string(),
64 reproducible_ordering: true,
65 freshness_affects_ranking: true,
66 visibility_only: true,
67 explicit_trust_activation_required: true,
68 ranking_inputs: vec![
69 "freshness".to_string(),
70 "status".to_string(),
71 "actor_kind".to_string(),
72 "actor_id".to_string(),
73 "published_at_desc".to_string(),
74 "publisher_role".to_string(),
75 "listing_id".to_string(),
76 ],
77 }
78 }
79}
80
81impl GenericListingSearchPolicy {
82 pub fn validate(&self) -> Result<(), String> {
83 validate_non_empty(&self.algorithm, "search_policy.algorithm")?;
84 if !self.reproducible_ordering {
85 return Err("generic listing search must remain reproducible".to_string());
86 }
87 if !self.visibility_only {
88 return Err("generic listing search must remain visibility-only".to_string());
89 }
90 if !self.explicit_trust_activation_required {
91 return Err(
92 "generic listing search must require explicit trust activation outside search"
93 .to_string(),
94 );
95 }
96 Ok(())
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101#[serde(rename_all = "camelCase")]
102pub struct GenericListingReplicaFreshness {
103 pub state: GenericListingFreshnessState,
104 pub age_secs: u64,
105 pub max_age_secs: u64,
106 pub valid_until: u64,
107 pub generated_at: u64,
108}
109
110impl GenericListingReplicaFreshness {
111 pub fn validate(&self) -> Result<(), String> {
112 if self.max_age_secs == 0 {
113 return Err("freshness.max_age_secs must be greater than zero".to_string());
114 }
115 if self.valid_until <= self.generated_at {
116 return Err("freshness.valid_until must be greater than generated_at".to_string());
117 }
118 Ok(())
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
123#[serde(rename_all = "camelCase")]
124pub struct GenericListingSearchResult {
125 pub rank: u64,
126 pub listing: SignedGenericListing,
127 pub publisher: GenericRegistryPublisher,
128 pub freshness: GenericListingReplicaFreshness,
129 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub replica_operator_ids: Vec<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
134#[serde(rename_all = "camelCase")]
135pub struct GenericListingSearchError {
136 pub operator_id: String,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub operator_name: Option<String>,
139 pub registry_url: String,
140 pub error: String,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144#[serde(rename_all = "camelCase")]
145pub struct GenericListingDivergence {
146 pub divergence_key: String,
147 pub actor_id: String,
148 pub actor_kind: GenericListingActorKind,
149 pub publisher_operator_ids: Vec<String>,
150 pub reason: String,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
154#[serde(rename_all = "camelCase")]
155pub struct GenericListingSearchResponse {
156 pub schema: String,
157 pub generated_at: u64,
158 pub query: GenericListingQuery,
159 pub search_policy: GenericListingSearchPolicy,
160 pub peer_count: u64,
161 pub reachable_count: u64,
162 pub stale_peer_count: u64,
163 pub divergence_count: u64,
164 pub result_count: u64,
165 pub results: Vec<GenericListingSearchResult>,
166 pub divergences: Vec<GenericListingDivergence>,
167 pub errors: Vec<GenericListingSearchError>,
168}