1use std::collections::HashMap;
14use std::sync::Arc;
15
16use async_trait::async_trait;
17use http::{Method, StatusCode};
18use percent_encoding::percent_decode_str;
19use serde_json::{Map, Value};
20use tokio::sync::Mutex as AsyncMutex;
21
22use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
23use fakecloud_persistence::SnapshotStore;
24
25use crate::generated::{OpMeta, Seg, Verb, K, OPS};
26use crate::persistence::save_snapshot;
27use crate::state::SharedIotWirelessState;
28use crate::validate::{self, Inputs};
29
30mod engine;
31mod special;
32#[cfg(test)]
33mod tests;
34
35pub use crate::generated::ACTIONS as IOTWIRELESS_ACTIONS;
37
38pub struct IotWirelessService {
39 state: SharedIotWirelessState,
40 snapshot_store: Option<Arc<dyn SnapshotStore>>,
41 snapshot_lock: Arc<AsyncMutex<()>>,
42}
43
44impl IotWirelessService {
45 pub fn new(state: SharedIotWirelessState) -> Self {
46 Self {
47 state,
48 snapshot_store: None,
49 snapshot_lock: Arc::new(AsyncMutex::new(())),
50 }
51 }
52
53 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
54 self.snapshot_store = Some(store);
55 self
56 }
57
58 async fn save(&self) {
59 save_snapshot(
60 &self.state,
61 self.snapshot_store.clone(),
62 &self.snapshot_lock,
63 )
64 .await;
65 }
66
67 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
69 let store = self.snapshot_store.clone()?;
70 let state = self.state.clone();
71 let lock = self.snapshot_lock.clone();
72 Some(Arc::new(move || {
73 let state = state.clone();
74 let store = store.clone();
75 let lock = lock.clone();
76 Box::pin(async move {
77 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
78 })
79 }))
80 }
81}
82
83#[async_trait]
84impl AwsService for IotWirelessService {
85 fn service_name(&self) -> &str {
86 "iotwireless"
87 }
88
89 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
90 let Some((meta, labels)) = match_route(&req.method, &req.raw_path) else {
91 return Err(AwsServiceError::aws_error(
92 StatusCode::NOT_FOUND,
93 "ResourceNotFoundException",
94 format!("Unknown operation: {} {}", req.method, req.raw_path),
95 ));
96 };
97 let result = self.dispatch(meta, &labels, &req);
98 match result {
99 Ok((resp, mutated)) => {
100 if mutated && resp.status.is_success() {
101 self.save().await;
102 }
103 Ok(resp)
104 }
105 Err(e) => Err(e),
106 }
107 }
108
109 fn supported_actions(&self) -> &[&str] {
110 IOTWIRELESS_ACTIONS
111 }
112}
113
114pub(crate) struct Ctx {
116 pub account: String,
117 pub region: String,
118}
119
120impl IotWirelessService {
121 fn dispatch(
122 &self,
123 meta: &'static OpMeta,
124 labels: &HashMap<String, String>,
125 req: &AwsRequest,
126 ) -> Result<(AwsResponse, bool), AwsServiceError> {
127 for v in labels.values() {
129 if v.is_empty() || (v.starts_with('{') && v.ends_with('}')) {
130 return Err(validate::invalid(
131 meta,
132 "The request is missing a required path parameter.",
133 ));
134 }
135 }
136 let ctx = Ctx {
137 account: req.account_id.clone(),
138 region: if req.region.is_empty() {
139 "us-east-1".to_string()
140 } else {
141 req.region.clone()
142 },
143 };
144 let query = parse_query(&req.raw_query);
145 let body = parse_body(&req.body);
146 let inputs = Inputs {
147 labels,
148 query: &query,
149 headers: &req.headers,
150 body: &body,
151 };
152 validate::validate(meta, &inputs)?;
153
154 if let Some(res) = special::dispatch(
156 self,
157 meta,
158 &ctx,
159 labels,
160 &query,
161 &req.headers,
162 &req.body,
163 &body,
164 )? {
165 return Ok(res);
166 }
167
168 match meta.verb {
170 Verb::Create => {
171 let mut g = self.state.write();
172 let data = g.get_or_create(&ctx.account);
173 Ok((
174 engine::create(data, &ctx, meta, labels, &query, &body)?,
175 true,
176 ))
177 }
178 Verb::Update => {
179 let mut g = self.state.write();
180 let data = g.get_or_create(&ctx.account);
181 Ok((
182 engine::update(data, &ctx, meta, labels, &query, &body),
183 true,
184 ))
185 }
186 Verb::Delete => {
187 let mut g = self.state.write();
188 let data = g.get_or_create(&ctx.account);
189 Ok((engine::delete(data, meta, labels), true))
190 }
191 Verb::Get => {
192 let g = self.state.read();
193 let data = g.get(&ctx.account);
194 Ok((engine::get(data, meta, labels)?, false))
195 }
196 Verb::List => {
197 let g = self.state.read();
198 let data = g.get(&ctx.account);
199 Ok((engine::list(data, meta, &query), false))
200 }
201 Verb::Action => {
202 if is_read_only_action(meta) {
207 Ok((ok_json(Value::Object(Map::new())), false))
208 } else {
209 Err(AwsServiceError::action_not_implemented(
210 "iotwireless",
211 meta.op,
212 ))
213 }
214 }
215 }
216 }
217}
218
219pub(crate) fn match_route(
226 method: &Method,
227 raw_path: &str,
228) -> Option<(&'static OpMeta, HashMap<String, String>)> {
229 let raw = raw_path.split('?').next().unwrap_or(raw_path);
230 let trimmed = raw.strip_prefix('/').unwrap_or(raw);
231 let segs: Vec<String> = if trimmed.is_empty() {
232 Vec::new()
233 } else {
234 trimmed
235 .split('/')
236 .map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned())
237 .collect()
238 };
239 let method_str = method.as_str();
240
241 let mut best: Option<(&'static OpMeta, HashMap<String, String>, usize)> = None;
242 for meta in OPS {
243 if meta.method != method_str {
244 continue;
245 }
246 if let Some((labels, fixed)) = match_segs(meta.segs, &segs) {
247 if best.as_ref().map(|(_, _, f)| fixed > *f).unwrap_or(true) {
248 best = Some((meta, labels, fixed));
249 }
250 }
251 }
252 best.map(|(m, l, _)| (m, l))
253}
254
255fn match_segs(pattern: &[Seg], segs: &[String]) -> Option<(HashMap<String, String>, usize)> {
258 let has_greedy = matches!(pattern.last(), Some(Seg::Greedy(_)));
259 if has_greedy {
260 if segs.len() < pattern.len() {
261 return None;
262 }
263 } else if segs.len() != pattern.len() {
264 return None;
265 }
266
267 let mut labels = HashMap::new();
268 let mut fixed = 0usize;
269 let mut i = 0usize;
270 for (p, seg) in pattern.iter().enumerate() {
271 match seg {
272 Seg::Fixed(f) => {
273 if segs.get(i)?.as_str() != *f {
274 return None;
275 }
276 fixed += 1;
277 i += 1;
278 }
279 Seg::Label(name) => {
280 labels.insert((*name).to_string(), segs.get(i)?.clone());
281 i += 1;
282 }
283 Seg::Greedy(name) => {
284 debug_assert_eq!(p, pattern.len() - 1);
286 let rest = segs[i..].join("/");
287 labels.insert((*name).to_string(), rest);
288 i = segs.len();
289 }
290 }
291 }
292 if i != segs.len() {
293 return None;
294 }
295 Some((labels, fixed))
296}
297
298pub(crate) fn ok_json(v: Value) -> AwsResponse {
301 AwsResponse::json_value(StatusCode::OK, v)
302}
303
304pub(crate) fn is_read_only_action(meta: &OpMeta) -> bool {
309 meta.method == "GET"
310 || ["Get", "List", "Describe", "Search"]
311 .iter()
312 .any(|p| meta.op.starts_with(p))
313}
314
315pub(crate) fn parse_query(raw: &str) -> Vec<(String, String)> {
316 raw.split('&')
317 .filter(|p| !p.is_empty())
318 .map(|pair| {
319 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
320 (
321 percent_decode_str(k).decode_utf8_lossy().into_owned(),
322 percent_decode_str(v).decode_utf8_lossy().into_owned(),
323 )
324 })
325 .collect()
326}
327
328pub(crate) fn parse_body(body: &[u8]) -> Map<String, Value> {
329 if body.is_empty() {
330 return Map::new();
331 }
332 match serde_json::from_slice::<Value>(body) {
333 Ok(Value::Object(m)) => m,
334 _ => Map::new(),
335 }
336}
337
338pub(crate) fn now_epoch() -> Value {
344 let millis = chrono::Utc::now().timestamp_millis();
345 Value::from(millis as f64 / 1000.0)
346}
347
348pub(crate) fn query_get<'a>(q: &'a [(String, String)], key: &str) -> Option<&'a str> {
349 q.iter()
350 .find(|(k, _)| k == key)
351 .map(|(_, v)| v.as_str())
352 .filter(|v| !v.is_empty())
353}
354
355pub(crate) fn resource_type(meta: &OpMeta) -> String {
361 meta.segs
362 .iter()
363 .filter_map(|s| match s {
364 Seg::Fixed(f) => Some(*f),
365 _ => None,
366 })
367 .collect::<Vec<_>>()
368 .join("/")
369}
370
371pub(crate) fn storage_key(meta: &OpMeta, labels: &HashMap<String, String>) -> String {
374 let mut parts = Vec::new();
375 for s in meta.segs {
376 match s {
377 Seg::Label(name) | Seg::Greedy(name) => {
378 if let Some(v) = labels.get(*name) {
379 parts.push(v.clone());
380 }
381 }
382 _ => {}
383 }
384 }
385 parts.join("/")
386}
387
388pub(crate) fn arn_resource(rtype: &str) -> &'static str {
390 match rtype {
391 "destinations" => "Destination",
392 "device-profiles" => "DeviceProfile",
393 "service-profiles" => "ServiceProfile",
394 "fuota-tasks" => "FuotaTask",
395 "multicast-groups" => "MulticastGroup",
396 "network-analyzer-configurations" => "NetworkAnalyzerConfig",
397 "wireless-devices" => "WirelessDevice",
398 "wireless-gateways" => "WirelessGateway",
399 "wireless-gateway-task-definitions" => "WirelessGatewayTaskDefinition",
400 "wireless_device_import_task" => "ImportTask",
401 "partner-accounts" => "PartnerAccount",
402 _ => "Resource",
403 }
404}
405
406pub(crate) fn mint_arn(ctx: &Ctx, rtype: &str, id: &str) -> String {
407 format!(
408 "arn:aws:iotwireless:{}:{}:{}/{}",
409 ctx.region,
410 ctx.account,
411 arn_resource(rtype),
412 id
413 )
414}
415
416pub(crate) fn mint_uuid(seed: &str) -> String {
418 let h = fnv(seed);
419 let h2 = fnv(&format!("{seed}:2"));
420 format!(
421 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
422 (h >> 32) as u32,
423 (h >> 16) as u16,
424 h as u16,
425 (h2 >> 48) as u16,
426 h2 & 0xffff_ffff_ffff
427 )
428}
429
430fn fnv(s: &str) -> u64 {
431 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
432 for b in s.bytes() {
433 h ^= b as u64;
434 h = h.wrapping_mul(0x0100_0000_01b3);
435 }
436 h
437}
438
439pub(crate) fn kind_matches(kind: K, v: &Value) -> bool {
441 match kind {
442 K::Str | K::Blob => v.is_string(),
443 K::Ts => v.is_string() || v.is_number(),
446 K::Int | K::Num => v.is_number(),
447 K::Bool => v.is_boolean(),
448 K::List => v.is_array(),
449 K::Map | K::Struct => v.is_object(),
450 }
451}
452
453pub(crate) fn build_output(meta: &OpMeta, record: &Value) -> Value {
456 let mut out = Map::new();
457 if let Some(obj) = record.as_object() {
458 for (wire, kind) in meta.omembers {
459 if let Some(v) = obj.get(*wire) {
460 if !v.is_null() && kind_matches(*kind, v) {
461 out.insert((*wire).to_string(), v.clone());
462 }
463 }
464 }
465 }
466 Value::Object(out)
467}
468
469pub(crate) fn build_element(meta: &OpMeta, record: &Value) -> Value {
471 let mut out = Map::new();
472 if let Some(obj) = record.as_object() {
473 for (wire, kind) in meta.list_elems {
474 if let Some(v) = obj.get(*wire) {
475 if !v.is_null() && kind_matches(*kind, v) {
476 out.insert((*wire).to_string(), v.clone());
477 }
478 }
479 }
480 }
481 Value::Object(out)
482}