use std::collections::HashMap;
use http::StatusCode;
use serde_json::{Map, Value};
use fakecloud_core::service::{AwsResponse, AwsServiceError};
use crate::generated::{OpMeta, K};
use crate::state::IotData;
use super::{
build_element, build_output, mint_arn, mint_uuid, now_epoch, ok_json, query_get, resource_type,
storage_key, Ctx,
};
const DEFAULT_PAGE: usize = 250;
pub(super) fn not_found(meta: &OpMeta, key: &str) -> AwsServiceError {
if meta.errors.contains(&"ResourceNotFoundException") {
AwsServiceError::aws_error(
StatusCode::NOT_FOUND,
"ResourceNotFoundException",
format!("The resource '{key}' does not exist."),
)
} else {
crate::validate::invalid(meta, format!("The resource '{key}' does not exist."))
}
}
fn build_record(
ctx: &Ctx,
meta: &OpMeta,
rtype: &str,
key: &str,
labels: &HashMap<String, String>,
query: &[(String, String)],
body: &Map<String, Value>,
) -> Value {
let mut record: Map<String, Value> = body.clone();
for (name, val) in labels {
record.insert(name.clone(), Value::String(val.clone()));
}
for (k, v) in query {
record
.entry(k.clone())
.or_insert_with(|| Value::String(v.clone()));
}
let primary = key.rsplit('/').next().unwrap_or(key);
for (wire, kind) in meta.omembers {
if record.contains_key(*wire) {
continue;
}
match kind {
K::Str if wire.ends_with("Arn") => {
record.insert(
(*wire).to_string(),
Value::String(mint_arn(ctx, rtype, primary)),
);
}
K::Str if wire.ends_with("Id") => {
record.insert(
(*wire).to_string(),
Value::String(mint_uuid(&format!("{}:{}:{}", ctx.account, rtype, key))),
);
}
K::Ts => {
record.insert((*wire).to_string(), now_epoch());
}
_ => {}
}
}
apply_attribute_payload(&mut record);
Value::Object(record)
}
fn apply_removal_toggles(record: &mut Map<String, Value>) {
for (toggle, field) in [
("removeThingType", "thingTypeName"), ("deleteBehaviors", "behaviors"), ("deleteAlertTargets", "alertTargets"), ("removeAuthorizerConfig", "authorizerConfig"), ] {
let set = record
.remove(toggle)
.and_then(|v| v.as_bool())
.unwrap_or(false);
if set {
record.remove(field);
}
}
}
fn apply_attribute_payload(record: &mut Map<String, Value>) {
let Some(payload) = record.remove("attributePayload") else {
return;
};
let merge = payload
.get("merge")
.and_then(Value::as_bool)
.unwrap_or(false);
let incoming = payload
.get("attributes")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let mut attributes = if merge {
record
.get("attributes")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default()
} else {
Map::new()
};
for (k, v) in incoming {
if v.as_str() == Some("") {
attributes.remove(&k);
} else {
attributes.insert(k, v);
}
}
record.insert("attributes".to_string(), Value::Object(attributes));
}
pub(super) fn create(
data: &mut IotData,
ctx: &Ctx,
meta: &OpMeta,
labels: &HashMap<String, String>,
query: &[(String, String)],
body: &Map<String, Value>,
) -> Result<AwsResponse, AwsServiceError> {
let rtype = resource_type(meta);
let key = storage_key(meta, labels);
if data.get_resource(&rtype, &key).is_some() {
if meta.errors.contains(&"ResourceAlreadyExistsException") {
return Err(AwsServiceError::aws_error(
StatusCode::CONFLICT,
"ResourceAlreadyExistsException",
format!("The resource '{key}' already exists."),
));
}
if meta.errors.contains(&"ConflictException") {
return Err(AwsServiceError::aws_error(
StatusCode::CONFLICT,
"ConflictException",
format!("The resource '{key}' already exists."),
));
}
}
let record = build_record(ctx, meta, &rtype, &key, labels, query, body);
let out = build_output(meta, &record);
data.put_resource(&rtype, &key, record);
Ok(ok_json(out))
}
pub(super) fn update(
data: &mut IotData,
meta: &OpMeta,
labels: &HashMap<String, String>,
body: &Map<String, Value>,
) -> Result<AwsResponse, AwsServiceError> {
let rtype = resource_type(meta);
let key = storage_key(meta, labels);
let Some(mut record) = data.get_resource(&rtype, &key).cloned() else {
return Err(not_found(meta, &key));
};
if let Some(obj) = record.as_object_mut() {
for (k, v) in body {
obj.insert(k.clone(), v.clone());
}
apply_removal_toggles(obj);
apply_attribute_payload(obj);
obj.insert("lastModifiedDate".to_string(), now_epoch());
}
let out = build_output(meta, &record);
data.put_resource(&rtype, &key, record);
Ok(ok_json(out))
}
pub(super) fn delete(
data: &mut IotData,
meta: &OpMeta,
labels: &HashMap<String, String>,
) -> AwsResponse {
let rtype = resource_type(meta);
let key = storage_key(meta, labels);
data.remove_resource(&rtype, &key);
ok_json(Value::Object(Map::new()))
}
fn wrapper_field(op: &str) -> Option<&'static str> {
Some(match op {
"DescribeCertificate" | "DescribeCACertificate" => "certificateDescription",
"DescribeAuthorizer" => "authorizerDescription",
"DescribeRoleAlias" => "roleAliasDescription",
"DescribeStream" => "streamInfo",
"DescribeCertificateProvider" | "DescribeManagedJobTemplate" => return None,
_ => return None,
})
}
pub(super) fn get(
data: Option<&IotData>,
meta: &OpMeta,
labels: &HashMap<String, String>,
) -> Result<AwsResponse, AwsServiceError> {
let rtype = resource_type(meta);
let key = storage_key(meta, labels);
match data.and_then(|d| d.get_resource(&rtype, &key)) {
Some(record) => {
if let Some(field) = wrapper_field(meta.op) {
let mut out = Map::new();
out.insert(field.to_string(), record.clone());
Ok(ok_json(Value::Object(out)))
} else {
Ok(ok_json(build_output(meta, record)))
}
}
None => Err(not_found(meta, &key)),
}
}
pub(super) fn list(
data: Option<&IotData>,
meta: &OpMeta,
query: &[(String, String)],
) -> AwsResponse {
let rtype = resource_type(meta);
let entries = data
.map(|d| d.list_resource_entries(&rtype))
.unwrap_or_default();
let elements: Vec<Value> = if meta.list_scalar {
entries
.iter()
.map(|(id, _)| Value::String(id.clone()))
.collect()
} else {
entries
.iter()
.map(|(_, r)| build_element(meta, r))
.collect()
};
let page_size = query_get(query, "maxResults")
.or_else(|| query_get(query, "pageSize"))
.and_then(|v| v.parse::<usize>().ok())
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_PAGE);
let start = query_get(query, "nextToken")
.or_else(|| query_get(query, "marker"))
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0);
let end = (start + page_size).min(elements.len());
let page = elements.get(start..end).unwrap_or(&[]).to_vec();
let has_next = end < elements.len();
let mut out = Map::new();
if let Some(field) = meta.list_field {
out.insert(field.to_string(), Value::Array(page));
}
if has_next {
if meta.omembers.iter().any(|(w, _)| *w == "nextToken") {
out.insert("nextToken".to_string(), Value::String(end.to_string()));
} else if meta.omembers.iter().any(|(w, _)| *w == "nextMarker") {
out.insert("nextMarker".to_string(), Value::String(end.to_string()));
}
}
ok_json(Value::Object(out))
}