use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use http::{Method, StatusCode};
use percent_encoding::percent_decode_str;
use serde_json::{Map, Value};
use tokio::sync::Mutex as AsyncMutex;
use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
use fakecloud_persistence::SnapshotStore;
use crate::generated::{OpMeta, Seg, Verb, K, OPS};
use crate::persistence::save_snapshot;
use crate::state::SharedIotWirelessState;
use crate::validate::{self, Inputs};
mod engine;
mod special;
#[cfg(test)]
mod tests;
pub use crate::generated::ACTIONS as IOTWIRELESS_ACTIONS;
pub struct IotWirelessService {
state: SharedIotWirelessState,
snapshot_store: Option<Arc<dyn SnapshotStore>>,
snapshot_lock: Arc<AsyncMutex<()>>,
}
impl IotWirelessService {
pub fn new(state: SharedIotWirelessState) -> Self {
Self {
state,
snapshot_store: None,
snapshot_lock: Arc::new(AsyncMutex::new(())),
}
}
pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
self.snapshot_store = Some(store);
self
}
async fn save(&self) {
save_snapshot(
&self.state,
self.snapshot_store.clone(),
&self.snapshot_lock,
)
.await;
}
pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
let store = self.snapshot_store.clone()?;
let state = self.state.clone();
let lock = self.snapshot_lock.clone();
Some(Arc::new(move || {
let state = state.clone();
let store = store.clone();
let lock = lock.clone();
Box::pin(async move {
crate::persistence::save_snapshot(&state, Some(store), &lock).await;
})
}))
}
}
#[async_trait]
impl AwsService for IotWirelessService {
fn service_name(&self) -> &str {
"iotwireless"
}
async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
let Some((meta, labels)) = match_route(&req.method, &req.raw_path) else {
return Err(AwsServiceError::aws_error(
StatusCode::NOT_FOUND,
"ResourceNotFoundException",
format!("Unknown operation: {} {}", req.method, req.raw_path),
));
};
let result = self.dispatch(meta, &labels, &req);
match result {
Ok((resp, mutated)) => {
if mutated && resp.status.is_success() {
self.save().await;
}
Ok(resp)
}
Err(e) => Err(e),
}
}
fn supported_actions(&self) -> &[&str] {
IOTWIRELESS_ACTIONS
}
}
pub(crate) struct Ctx {
pub account: String,
pub region: String,
}
impl IotWirelessService {
fn dispatch(
&self,
meta: &'static OpMeta,
labels: &HashMap<String, String>,
req: &AwsRequest,
) -> Result<(AwsResponse, bool), AwsServiceError> {
for v in labels.values() {
if v.is_empty() || (v.starts_with('{') && v.ends_with('}')) {
return Err(validate::invalid(
meta,
"The request is missing a required path parameter.",
));
}
}
let ctx = Ctx {
account: req.account_id.clone(),
region: if req.region.is_empty() {
"us-east-1".to_string()
} else {
req.region.clone()
},
};
let query = parse_query(&req.raw_query);
let body = parse_body(&req.body);
let inputs = Inputs {
labels,
query: &query,
headers: &req.headers,
body: &body,
};
validate::validate(meta, &inputs)?;
if let Some(res) = special::dispatch(
self,
meta,
&ctx,
labels,
&query,
&req.headers,
&req.body,
&body,
)? {
return Ok(res);
}
match meta.verb {
Verb::Create => {
let mut g = self.state.write();
let data = g.get_or_create(&ctx.account);
Ok((
engine::create(data, &ctx, meta, labels, &query, &body)?,
true,
))
}
Verb::Update => {
let mut g = self.state.write();
let data = g.get_or_create(&ctx.account);
Ok((
engine::update(data, &ctx, meta, labels, &query, &body),
true,
))
}
Verb::Delete => {
let mut g = self.state.write();
let data = g.get_or_create(&ctx.account);
Ok((engine::delete(data, meta, labels), true))
}
Verb::Get => {
let g = self.state.read();
let data = g.get(&ctx.account);
Ok((engine::get(data, meta, labels)?, false))
}
Verb::List => {
let g = self.state.read();
let data = g.get(&ctx.account);
Ok((engine::list(data, meta, &query), false))
}
Verb::Action => {
if is_read_only_action(meta) {
Ok((ok_json(Value::Object(Map::new())), false))
} else {
Err(AwsServiceError::action_not_implemented(
"iotwireless",
meta.op,
))
}
}
}
}
}
pub(crate) fn match_route(
method: &Method,
raw_path: &str,
) -> Option<(&'static OpMeta, HashMap<String, String>)> {
let raw = raw_path.split('?').next().unwrap_or(raw_path);
let trimmed = raw.strip_prefix('/').unwrap_or(raw);
let segs: Vec<String> = if trimmed.is_empty() {
Vec::new()
} else {
trimmed
.split('/')
.map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned())
.collect()
};
let method_str = method.as_str();
let mut best: Option<(&'static OpMeta, HashMap<String, String>, usize)> = None;
for meta in OPS {
if meta.method != method_str {
continue;
}
if let Some((labels, fixed)) = match_segs(meta.segs, &segs) {
if best.as_ref().map(|(_, _, f)| fixed > *f).unwrap_or(true) {
best = Some((meta, labels, fixed));
}
}
}
best.map(|(m, l, _)| (m, l))
}
fn match_segs(pattern: &[Seg], segs: &[String]) -> Option<(HashMap<String, String>, usize)> {
let has_greedy = matches!(pattern.last(), Some(Seg::Greedy(_)));
if has_greedy {
if segs.len() < pattern.len() {
return None;
}
} else if segs.len() != pattern.len() {
return None;
}
let mut labels = HashMap::new();
let mut fixed = 0usize;
let mut i = 0usize;
for (p, seg) in pattern.iter().enumerate() {
match seg {
Seg::Fixed(f) => {
if segs.get(i)?.as_str() != *f {
return None;
}
fixed += 1;
i += 1;
}
Seg::Label(name) => {
labels.insert((*name).to_string(), segs.get(i)?.clone());
i += 1;
}
Seg::Greedy(name) => {
debug_assert_eq!(p, pattern.len() - 1);
let rest = segs[i..].join("/");
labels.insert((*name).to_string(), rest);
i = segs.len();
}
}
}
if i != segs.len() {
return None;
}
Some((labels, fixed))
}
pub(crate) fn ok_json(v: Value) -> AwsResponse {
AwsResponse::json_value(StatusCode::OK, v)
}
pub(crate) fn is_read_only_action(meta: &OpMeta) -> bool {
meta.method == "GET"
|| ["Get", "List", "Describe", "Search"]
.iter()
.any(|p| meta.op.starts_with(p))
}
pub(crate) fn parse_query(raw: &str) -> Vec<(String, String)> {
raw.split('&')
.filter(|p| !p.is_empty())
.map(|pair| {
let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
(
percent_decode_str(k).decode_utf8_lossy().into_owned(),
percent_decode_str(v).decode_utf8_lossy().into_owned(),
)
})
.collect()
}
pub(crate) fn parse_body(body: &[u8]) -> Map<String, Value> {
if body.is_empty() {
return Map::new();
}
match serde_json::from_slice::<Value>(body) {
Ok(Value::Object(m)) => m,
_ => Map::new(),
}
}
pub(crate) fn now_epoch() -> Value {
let millis = chrono::Utc::now().timestamp_millis();
Value::from(millis as f64 / 1000.0)
}
pub(crate) fn query_get<'a>(q: &'a [(String, String)], key: &str) -> Option<&'a str> {
q.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
.filter(|v| !v.is_empty())
}
pub(crate) fn resource_type(meta: &OpMeta) -> String {
meta.segs
.iter()
.filter_map(|s| match s {
Seg::Fixed(f) => Some(*f),
_ => None,
})
.collect::<Vec<_>>()
.join("/")
}
pub(crate) fn storage_key(meta: &OpMeta, labels: &HashMap<String, String>) -> String {
let mut parts = Vec::new();
for s in meta.segs {
match s {
Seg::Label(name) | Seg::Greedy(name) => {
if let Some(v) = labels.get(*name) {
parts.push(v.clone());
}
}
_ => {}
}
}
parts.join("/")
}
pub(crate) fn arn_resource(rtype: &str) -> &'static str {
match rtype {
"destinations" => "Destination",
"device-profiles" => "DeviceProfile",
"service-profiles" => "ServiceProfile",
"fuota-tasks" => "FuotaTask",
"multicast-groups" => "MulticastGroup",
"network-analyzer-configurations" => "NetworkAnalyzerConfig",
"wireless-devices" => "WirelessDevice",
"wireless-gateways" => "WirelessGateway",
"wireless-gateway-task-definitions" => "WirelessGatewayTaskDefinition",
"wireless_device_import_task" => "ImportTask",
"partner-accounts" => "PartnerAccount",
_ => "Resource",
}
}
pub(crate) fn mint_arn(ctx: &Ctx, rtype: &str, id: &str) -> String {
format!(
"arn:aws:iotwireless:{}:{}:{}/{}",
ctx.region,
ctx.account,
arn_resource(rtype),
id
)
}
pub(crate) fn mint_uuid(seed: &str) -> String {
let h = fnv(seed);
let h2 = fnv(&format!("{seed}:2"));
format!(
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
(h >> 32) as u32,
(h >> 16) as u16,
h as u16,
(h2 >> 48) as u16,
h2 & 0xffff_ffff_ffff
)
}
fn fnv(s: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in s.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0100_0000_01b3);
}
h
}
pub(crate) fn kind_matches(kind: K, v: &Value) -> bool {
match kind {
K::Str | K::Blob => v.is_string(),
K::Ts => v.is_string() || v.is_number(),
K::Int | K::Num => v.is_number(),
K::Bool => v.is_boolean(),
K::List => v.is_array(),
K::Map | K::Struct => v.is_object(),
}
}
pub(crate) fn build_output(meta: &OpMeta, record: &Value) -> Value {
let mut out = Map::new();
if let Some(obj) = record.as_object() {
for (wire, kind) in meta.omembers {
if let Some(v) = obj.get(*wire) {
if !v.is_null() && kind_matches(*kind, v) {
out.insert((*wire).to_string(), v.clone());
}
}
}
}
Value::Object(out)
}
pub(crate) fn build_element(meta: &OpMeta, record: &Value) -> Value {
let mut out = Map::new();
if let Some(obj) = record.as_object() {
for (wire, kind) in meta.list_elems {
if let Some(v) = obj.get(*wire) {
if !v.is_null() && kind_matches(*kind, v) {
out.insert((*wire).to_string(), v.clone());
}
}
}
}
Value::Object(out)
}