futu_cache/event_contract/
catalog.rs1use super::*;
2
3impl EventContractCache {
4 #[must_use]
5 pub fn is_category_ready(&self) -> bool {
6 !self.category.load().categories.is_empty()
7 }
8
9 #[must_use]
10 pub fn has_competition_generation(&self, category_id: u32) -> bool {
11 self.category
12 .load()
13 .competitions_by_category
14 .contains_key(&category_id)
15 }
16
17 pub fn apply_category_response(
18 &self,
19 body: &[u8],
20 app_lang: i32,
21 ) -> Result<(), EventContractCacheError> {
22 let response = GetEventCategoryResponse::decode(body)
23 .map_err(|error| EventContractCacheError::Decode(error.to_string()))?;
24 if response.ret_code != Some(0) {
25 return Err(EventContractCacheError::Validation(
26 "event contract category backend ret_code is not success",
27 ));
28 }
29
30 let _writer = self.category_writer.lock();
31 let previous = self.category_snapshot();
32 let mut next = CategoryGeneration {
33 competitions_by_category: previous.competitions_by_category.clone(),
34 ..CategoryGeneration::default()
35 };
36 let mut category_ids = std::collections::BTreeSet::new();
37 let mut category_values = std::collections::BTreeSet::new();
38
39 for item in response.mix_category_items {
40 let id = required_nonzero(item.category_id, "category_id is missing or zero")?;
41 let value = required_string(item.category, "category is missing or empty")?;
42 if !category_ids.insert(id) || !category_values.insert(value.clone()) {
43 return Err(EventContractCacheError::Validation(
44 "duplicate event contract category",
45 ));
46 }
47 next.categories.push(Category {
48 id,
49 value,
50 name: localized(item.name.as_ref(), app_lang),
51 });
52
53 let mut tags_by_value = std::collections::BTreeMap::<String, Tag>::new();
54 for mix in item.mix_items {
55 let Some(value) = mix.tag.filter(|value| !value.is_empty()) else {
56 continue;
57 };
58 let tag_id = mix
59 .sub_category_item
60 .as_ref()
61 .and_then(|tag| tag.sub_category_id)
62 .or(mix.mix_item_id)
63 .unwrap_or(0);
64 if tag_id == 0 {
67 continue;
68 }
69 let entry = tags_by_value.entry(value.clone()).or_insert_with(|| Tag {
70 id: tag_id,
71 category_id: id,
72 value,
73 name: localized(mix.name.as_ref(), app_lang),
74 scopes: Vec::new(),
75 });
76 for scope in mix.competition_scopes {
77 let scope_id =
78 required_nonzero(scope.id, "competition scope id is missing or zero")?;
79 let scope_value = required_string(
80 scope.competition_scope,
81 "competition scope is missing or empty",
82 )?;
83 if entry
84 .scopes
85 .iter()
86 .all(|candidate| candidate.id != scope_id)
87 {
88 entry.scopes.push(Scope {
89 id: scope_id,
90 value: scope_value,
91 name: localized(scope.name.as_ref(), app_lang),
92 });
93 }
94 }
95 }
96 for sub in item.sub_category_items {
97 let value = required_string(sub.tag, "sub-category tag is missing or empty")?;
98 if tags_by_value.contains_key(&value) {
99 continue;
100 }
101 let tag_id =
102 required_nonzero(sub.sub_category_id, "sub-category id is missing or zero")?;
103 tags_by_value.insert(
104 value.clone(),
105 Tag {
106 id: tag_id,
107 category_id: id,
108 value,
109 name: localized(sub.name.as_ref(), app_lang),
110 scopes: Vec::new(),
111 },
112 );
113 }
114 next.tags.extend(tags_by_value.into_values());
115 }
116 if next.categories.is_empty() {
117 return Err(EventContractCacheError::Validation(
118 "event contract category response is empty",
119 ));
120 }
121 self.category.store(Arc::new(next));
122 Ok(())
123 }
124
125 pub fn apply_competition_response(
126 &self,
127 category_id: u32,
128 body: &[u8],
129 app_lang: i32,
130 ) -> Result<(), EventContractCacheError> {
131 if category_id == 0 {
132 return Err(EventContractCacheError::Validation(
133 "competition category id is zero",
134 ));
135 }
136 let response = GetCompetitionMixListResponse::decode(body)
137 .map_err(|error| EventContractCacheError::Decode(error.to_string()))?;
138 if response.ret_code != Some(0) {
139 return Err(EventContractCacheError::Validation(
140 "event contract competition backend ret_code is not success",
141 ));
142 }
143 let _writer = self.category_writer.lock();
144 let previous = self.category_snapshot();
145 if previous
146 .categories
147 .iter()
148 .all(|category| category.id != category_id)
149 {
150 return Err(EventContractCacheError::Validation(
151 "competition category is absent from category generation",
152 ));
153 }
154
155 let mut next = (*previous).clone();
156 let mut rows = Vec::new();
157 for group in response.sub_category_groups {
158 let Some(sub) = group.sub_category else {
159 continue;
160 };
161 let tag_id =
162 required_nonzero(sub.sub_category_id, "competition tag id is missing or zero")?;
163 let tag = required_string(sub.tag, "competition tag is missing or empty")?;
164
165 if let Some(cached_tag) = next
166 .tags
167 .iter_mut()
168 .find(|candidate| candidate.category_id == category_id && candidate.value == tag)
169 {
170 cached_tag.id = tag_id;
171 }
172 for content in group.content_items {
173 if content.content_type != Some(3) {
176 continue;
177 }
178 let Some(competition) = content.competition else {
179 return Err(EventContractCacheError::Validation(
180 "competition content lacks competition payload",
181 ));
182 };
183 rows.push(Competition {
184 id: required_nonzero(competition.id, "competition id is missing or zero")?,
185 category_id,
186 tag_id,
187 tag: tag.clone(),
188 value: required_string(
189 competition.competition,
190 "competition value is missing or empty",
191 )?,
192 name: localized(competition.name.as_ref(), app_lang),
193 });
194 }
195 }
196 next.competitions_by_category.insert(category_id, rows);
197 self.category.store(Arc::new(next));
198 Ok(())
199 }
200}
201
202fn required_nonzero(
203 value: Option<u32>,
204 message: &'static str,
205) -> Result<u32, EventContractCacheError> {
206 value
207 .filter(|value| *value != 0)
208 .ok_or(EventContractCacheError::Validation(message))
209}
210
211fn required_string(
212 value: Option<String>,
213 message: &'static str,
214) -> Result<String, EventContractCacheError> {
215 value
216 .filter(|value| !value.is_empty())
217 .ok_or(EventContractCacheError::Validation(message))
218}
219
220pub(super) fn localized(value: Option<&LocaleString>, app_lang: i32) -> String {
221 let Some(value) = value else {
222 return String::new();
223 };
224 [app_lang, 2, 0, 1, 3]
228 .into_iter()
229 .find_map(|language_id| {
230 value.str_context.iter().find_map(|item| {
231 (item.language_id == Some(language_id))
232 .then(|| item.context.clone())
233 .flatten()
234 .filter(|context| !context.is_empty())
235 })
236 })
237 .or_else(|| {
238 value
239 .str_context
240 .iter()
241 .find_map(|item| item.context.clone().filter(|context| !context.is_empty()))
242 })
243 .unwrap_or_default()
244}