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 Ok((ok_json(Value::Object(Map::new())), false))
205 }
206 }
207 }
208}
209
210pub(crate) fn match_route(
217 method: &Method,
218 raw_path: &str,
219) -> Option<(&'static OpMeta, HashMap<String, String>)> {
220 let raw = raw_path.split('?').next().unwrap_or(raw_path);
221 let trimmed = raw.strip_prefix('/').unwrap_or(raw);
222 let segs: Vec<String> = if trimmed.is_empty() {
223 Vec::new()
224 } else {
225 trimmed
226 .split('/')
227 .map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned())
228 .collect()
229 };
230 let method_str = method.as_str();
231
232 let mut best: Option<(&'static OpMeta, HashMap<String, String>, usize)> = None;
233 for meta in OPS {
234 if meta.method != method_str {
235 continue;
236 }
237 if let Some((labels, fixed)) = match_segs(meta.segs, &segs) {
238 if best.as_ref().map(|(_, _, f)| fixed > *f).unwrap_or(true) {
239 best = Some((meta, labels, fixed));
240 }
241 }
242 }
243 best.map(|(m, l, _)| (m, l))
244}
245
246fn match_segs(pattern: &[Seg], segs: &[String]) -> Option<(HashMap<String, String>, usize)> {
249 let has_greedy = matches!(pattern.last(), Some(Seg::Greedy(_)));
250 if has_greedy {
251 if segs.len() < pattern.len() {
252 return None;
253 }
254 } else if segs.len() != pattern.len() {
255 return None;
256 }
257
258 let mut labels = HashMap::new();
259 let mut fixed = 0usize;
260 let mut i = 0usize;
261 for (p, seg) in pattern.iter().enumerate() {
262 match seg {
263 Seg::Fixed(f) => {
264 if segs.get(i)?.as_str() != *f {
265 return None;
266 }
267 fixed += 1;
268 i += 1;
269 }
270 Seg::Label(name) => {
271 labels.insert((*name).to_string(), segs.get(i)?.clone());
272 i += 1;
273 }
274 Seg::Greedy(name) => {
275 debug_assert_eq!(p, pattern.len() - 1);
277 let rest = segs[i..].join("/");
278 labels.insert((*name).to_string(), rest);
279 i = segs.len();
280 }
281 }
282 }
283 if i != segs.len() {
284 return None;
285 }
286 Some((labels, fixed))
287}
288
289pub(crate) fn ok_json(v: Value) -> AwsResponse {
292 AwsResponse::json_value(StatusCode::OK, v)
293}
294
295pub(crate) fn parse_query(raw: &str) -> Vec<(String, String)> {
296 raw.split('&')
297 .filter(|p| !p.is_empty())
298 .map(|pair| {
299 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
300 (
301 percent_decode_str(k).decode_utf8_lossy().into_owned(),
302 percent_decode_str(v).decode_utf8_lossy().into_owned(),
303 )
304 })
305 .collect()
306}
307
308pub(crate) fn parse_body(body: &[u8]) -> Map<String, Value> {
309 if body.is_empty() {
310 return Map::new();
311 }
312 match serde_json::from_slice::<Value>(body) {
313 Ok(Value::Object(m)) => m,
314 _ => Map::new(),
315 }
316}
317
318pub(crate) fn now_epoch() -> Value {
324 let millis = chrono::Utc::now().timestamp_millis();
325 Value::from(millis as f64 / 1000.0)
326}
327
328pub(crate) fn query_get<'a>(q: &'a [(String, String)], key: &str) -> Option<&'a str> {
329 q.iter()
330 .find(|(k, _)| k == key)
331 .map(|(_, v)| v.as_str())
332 .filter(|v| !v.is_empty())
333}
334
335pub(crate) fn resource_type(meta: &OpMeta) -> String {
341 meta.segs
342 .iter()
343 .filter_map(|s| match s {
344 Seg::Fixed(f) => Some(*f),
345 _ => None,
346 })
347 .collect::<Vec<_>>()
348 .join("/")
349}
350
351pub(crate) fn storage_key(meta: &OpMeta, labels: &HashMap<String, String>) -> String {
354 let mut parts = Vec::new();
355 for s in meta.segs {
356 match s {
357 Seg::Label(name) | Seg::Greedy(name) => {
358 if let Some(v) = labels.get(*name) {
359 parts.push(v.clone());
360 }
361 }
362 _ => {}
363 }
364 }
365 parts.join("/")
366}
367
368pub(crate) fn arn_resource(rtype: &str) -> &'static str {
370 match rtype {
371 "destinations" => "Destination",
372 "device-profiles" => "DeviceProfile",
373 "service-profiles" => "ServiceProfile",
374 "fuota-tasks" => "FuotaTask",
375 "multicast-groups" => "MulticastGroup",
376 "network-analyzer-configurations" => "NetworkAnalyzerConfig",
377 "wireless-devices" => "WirelessDevice",
378 "wireless-gateways" => "WirelessGateway",
379 "wireless-gateway-task-definitions" => "WirelessGatewayTaskDefinition",
380 "wireless_device_import_task" => "ImportTask",
381 "partner-accounts" => "PartnerAccount",
382 _ => "Resource",
383 }
384}
385
386pub(crate) fn mint_arn(ctx: &Ctx, rtype: &str, id: &str) -> String {
387 format!(
388 "arn:aws:iotwireless:{}:{}:{}/{}",
389 ctx.region,
390 ctx.account,
391 arn_resource(rtype),
392 id
393 )
394}
395
396pub(crate) fn mint_uuid(seed: &str) -> String {
398 let h = fnv(seed);
399 let h2 = fnv(&format!("{seed}:2"));
400 format!(
401 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
402 (h >> 32) as u32,
403 (h >> 16) as u16,
404 h as u16,
405 (h2 >> 48) as u16,
406 h2 & 0xffff_ffff_ffff
407 )
408}
409
410fn fnv(s: &str) -> u64 {
411 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
412 for b in s.bytes() {
413 h ^= b as u64;
414 h = h.wrapping_mul(0x0100_0000_01b3);
415 }
416 h
417}
418
419pub(crate) fn kind_matches(kind: K, v: &Value) -> bool {
421 match kind {
422 K::Str | K::Blob => v.is_string(),
423 K::Ts => v.is_string() || v.is_number(),
426 K::Int | K::Num => v.is_number(),
427 K::Bool => v.is_boolean(),
428 K::List => v.is_array(),
429 K::Map | K::Struct => v.is_object(),
430 }
431}
432
433pub(crate) fn build_output(meta: &OpMeta, record: &Value) -> Value {
436 let mut out = Map::new();
437 if let Some(obj) = record.as_object() {
438 for (wire, kind) in meta.omembers {
439 if let Some(v) = obj.get(*wire) {
440 if !v.is_null() && kind_matches(*kind, v) {
441 out.insert((*wire).to_string(), v.clone());
442 }
443 }
444 }
445 }
446 Value::Object(out)
447}
448
449pub(crate) fn build_element(meta: &OpMeta, record: &Value) -> Value {
451 let mut out = Map::new();
452 if let Some(obj) = record.as_object() {
453 for (wire, kind) in meta.list_elems {
454 if let Some(v) = obj.get(*wire) {
455 if !v.is_null() && kind_matches(*kind, v) {
456 out.insert((*wire).to_string(), v.clone());
457 }
458 }
459 }
460 }
461 Value::Object(out)
462}