use serde_json::{Map, Value};
use fakecloud_core::service::{AwsResponse, AwsServiceError};
use crate::generated::{Field, OpMeta, K};
use crate::state::SageMakerData;
use super::{in_use, mint_arn, not_found, now_epoch, ok_json, Ctx};
const DEFAULT_PAGE: usize = 100;
const PREFERRED_ENUMS: &[&str] = &[
"Completed",
"InService",
"Available",
"Active",
"Succeeded",
"Enabled",
"Success",
"Stopped",
"Deleted",
];
fn pick_enum(enums: &[&str]) -> String {
for p in PREFERRED_ENUMS {
if enums.contains(p) {
return (*p).to_string();
}
}
enums.first().copied().unwrap_or("Unknown").to_string()
}
fn synth_field(ctx: &Ctx, meta: &OpMeta, key: &str, f: &Field) -> Value {
match f.kind {
K::Str | K::Blob => Value::String(synth_string(ctx, meta, key, f)),
K::Ts => now_epoch(),
K::Int => Value::from(f.min_val.unwrap_or(1)),
K::Num => Value::from(f.min_val.unwrap_or(1) as f64),
K::Bool => Value::Bool(true),
K::Map => Value::Object(Map::new()),
K::Struct if f.is_union => {
let mut m = Map::new();
if let Some(child) = f.children.first() {
m.insert(child.wire.to_string(), synth_field(ctx, meta, key, child));
}
Value::Object(m)
}
K::Struct => {
let mut m = Map::new();
fill_required(ctx, meta, key, &mut m, f.children);
Value::Object(m)
}
K::List => {
if f.list_min.unwrap_or(0) >= 1 {
Value::Array(vec![synth_element(ctx, meta, key, f)])
} else {
Value::Array(Vec::new())
}
}
}
}
fn synth_element(ctx: &Ctx, meta: &OpMeta, key: &str, f: &Field) -> Value {
match f.elem_kind {
K::Struct => {
let mut m = Map::new();
fill_required(ctx, meta, key, &mut m, f.children);
Value::Object(m)
}
K::Str | K::Blob => {
if f.enums.is_empty() {
Value::String("placeholder".to_string())
} else {
Value::String(pick_enum(f.enums))
}
}
K::Ts => now_epoch(),
K::Int => Value::from(1),
K::Num => Value::from(1.0),
K::Bool => Value::Bool(true),
K::Map => Value::Object(Map::new()),
K::List => Value::Array(Vec::new()),
}
}
fn synth_string(ctx: &Ctx, meta: &OpMeta, key: &str, f: &Field) -> String {
if !f.enums.is_empty() {
return pick_enum(f.enums);
}
if f.wire.ends_with("Arn") {
return mint_arn(ctx, meta.arn_path, key);
}
if f.wire.ends_with("Name") && !key.is_empty() {
return key.to_string();
}
if f.wire.ends_with("Id") && !key.is_empty() {
return key.to_string();
}
let min = f.min_len.unwrap_or(1).max(1) as usize;
"a".repeat(min.max(1))
}
fn fill_required(
ctx: &Ctx,
meta: &OpMeta,
key: &str,
out: &mut Map<String, Value>,
fields: &[Field],
) {
for f in fields {
let needs_default = match out.get(f.wire) {
None => true,
Some(v) => v.is_null() || !kind_matches(f.kind, v),
};
if needs_default {
out.insert(f.wire.to_string(), synth_field(ctx, meta, key, f));
continue;
}
if let Some(v) = out.get_mut(f.wire) {
match f.kind {
K::Struct if f.is_union => {}
K::Struct => {
if let Some(m) = v.as_object_mut() {
fill_required(ctx, meta, key, m, f.children);
}
}
K::List if f.elem_kind == K::Struct => {
if let Some(arr) = v.as_array_mut() {
for el in arr.iter_mut() {
if let Some(m) = el.as_object_mut() {
fill_required(ctx, meta, key, m, f.children);
}
}
}
}
_ => {}
}
}
}
}
pub(crate) fn kind_matches(kind: K, v: &Value) -> bool {
match kind {
K::Str | K::Blob => v.is_string(),
K::Ts => 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(),
}
}
fn coerce_ts(v: &Value) -> Option<Value> {
if v.is_number() {
return Some(v.clone());
}
let s = v.as_str()?;
if let Ok(n) = s.parse::<f64>() {
return Some(Value::from(n));
}
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
return Some(Value::from(dt.timestamp_millis() as f64 / 1000.0));
}
None
}
fn project(obj: Option<&Map<String, Value>>, members: &[(&str, K)], out: &mut Map<String, Value>) {
let Some(obj) = obj else { return };
for (wire, kind) in members {
let Some(v) = obj.get(*wire) else { continue };
if v.is_null() {
continue;
}
if *kind == K::Ts {
if let Some(n) = coerce_ts(v) {
out.insert((*wire).to_string(), n);
}
} else if kind_matches(*kind, v) {
out.insert((*wire).to_string(), v.clone());
}
}
}
pub(crate) fn build_output(ctx: &Ctx, meta: &OpMeta, key: &str, record: &Value) -> Value {
let mut out = Map::new();
project(record.as_object(), meta.omembers, &mut out);
fill_required(ctx, meta, key, &mut out, meta.req_out);
Value::Object(out)
}
fn build_element(ctx: &Ctx, meta: &OpMeta, key: &str, record: &Value) -> Value {
let mut out = Map::new();
project(record.as_object(), meta.list_elems, &mut out);
fill_required(ctx, meta, key, &mut out, meta.req_elem);
Value::Object(out)
}
fn key_value(meta: &OpMeta, body: &Map<String, Value>) -> Option<String> {
if meta.key_member.is_empty() {
return None;
}
body.get(meta.key_member)
.and_then(Value::as_str)
.map(str::to_string)
}
fn build_record(ctx: &Ctx, meta: &OpMeta, key: &str, body: &Map<String, Value>) -> Value {
let mut record: Map<String, Value> = body.clone();
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, meta.arn_path, key)),
);
}
K::Str if wire.ends_with("Id") => {
record.insert(
(*wire).to_string(),
Value::String(super::mint_id(&ctx.account, meta.family, key)),
);
}
K::Ts => {
record.insert((*wire).to_string(), now_epoch());
}
_ => {}
}
}
let primary_arn = format!("{}Arn", meta.family);
record
.entry(primary_arn)
.or_insert_with(|| Value::String(mint_arn(ctx, meta.arn_path, key)));
let now = now_epoch();
record
.entry("CreationTime".to_string())
.or_insert_with(|| now.clone());
record.entry("LastModifiedTime".to_string()).or_insert(now);
Value::Object(record)
}
pub(super) fn create(
data: &mut SageMakerData,
ctx: &Ctx,
meta: &OpMeta,
body: &Map<String, Value>,
) -> Result<AwsResponse, AwsServiceError> {
let key = match key_value(meta, body) {
Some(k) => k,
None => super::mint_id(&ctx.account, meta.family, &data.next_seq().to_string()),
};
if data.get_resource(meta.family, &key).is_some() {
if meta.errors.contains(&"ResourceInUse") {
return Err(in_use(format!("Resource '{key}' already exists.")));
}
if meta.errors.contains(&"ConflictException") {
return Err(AwsServiceError::aws_error(
http::StatusCode::CONFLICT,
"ConflictException",
format!("Resource '{key}' already exists."),
));
}
}
let record = build_record(ctx, meta, &key, body);
let out = build_output(ctx, meta, &key, &record);
data.put_resource(meta.family, &key, record);
Ok(ok_json(out))
}
pub(super) fn update(
data: &mut SageMakerData,
ctx: &Ctx,
meta: &OpMeta,
body: &Map<String, Value>,
) -> Result<AwsResponse, AwsServiceError> {
let value = key_value(meta, body).unwrap_or_default();
let Some(key) = data.resolve_key(meta.family, &value) else {
return Err(not_found(format!("Resource '{value}' does not exist.")));
};
let mut record = data
.get_resource(meta.family, &key)
.cloned()
.unwrap_or(Value::Null);
if let Some(obj) = record.as_object_mut() {
for (k, v) in body {
obj.insert(k.clone(), v.clone());
}
obj.insert("LastModifiedTime".to_string(), now_epoch());
}
let out = build_output(ctx, meta, &key, &record);
data.put_resource(meta.family, &key, record);
Ok(ok_json(out))
}
pub(super) fn delete(
data: &mut SageMakerData,
ctx: &Ctx,
meta: &OpMeta,
body: &Map<String, Value>,
) -> AwsResponse {
let value = key_value(meta, body).unwrap_or_default();
if let Some(key) = data.resolve_key(meta.family, &value) {
data.remove_resource(meta.family, &key);
}
ok_json(build_output(ctx, meta, &value, &Value::Object(Map::new())))
}
pub(super) fn get(
data: Option<&SageMakerData>,
ctx: &Ctx,
meta: &OpMeta,
body: &Map<String, Value>,
) -> Result<AwsResponse, AwsServiceError> {
let value = key_value(meta, body).unwrap_or_default();
let resolved = data.and_then(|d| {
d.resolve_key(meta.family, &value)
.map(|k| (k.clone(), d.get_resource(meta.family, &k).cloned()))
});
match resolved {
Some((key, Some(record))) => Ok(ok_json(build_output(ctx, meta, &key, &record))),
_ => Err(not_found(format!("Resource '{value}' does not exist."))),
}
}
pub(super) fn action_key(body: &Map<String, Value>) -> String {
let str_members: Vec<(&String, &str)> = body
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k, s)))
.collect();
for suffix in ["Arn", "Name", "Id"] {
if let Some((_, s)) = str_members.iter().find(|(k, _)| k.ends_with(suffix)) {
return (*s).to_string();
}
}
str_members
.first()
.map(|(_, s)| (*s).to_string())
.unwrap_or_default()
}
pub(super) fn action(ctx: &Ctx, meta: &OpMeta, body: &Map<String, Value>) -> AwsResponse {
let key = action_key(body);
let out = build_output(ctx, meta, &key, &Value::Object(body.clone()));
ok_json(out)
}
pub(super) fn action_create(
data: &mut SageMakerData,
ctx: &Ctx,
meta: &OpMeta,
key: &str,
seed: &Map<String, Value>,
) -> AwsResponse {
let record = build_record(ctx, meta, key, seed);
let out = build_output(ctx, meta, key, &record);
data.put_resource(meta.family, key, record);
ok_json(out)
}
fn passes_filters(id: &str, record: &Value, body: &Map<String, Value>) -> bool {
let obj = record.as_object();
if let Some(needle) = body.get("NameContains").and_then(Value::as_str) {
let hit = id.contains(needle)
|| obj
.map(|o| {
o.iter().any(|(k, v)| {
k.ends_with("Name") && v.as_str().is_some_and(|s| s.contains(needle))
})
})
.unwrap_or(false);
if !hit {
return false;
}
}
let creation = obj
.and_then(|o| o.get("CreationTime"))
.and_then(Value::as_f64);
if let Some(after) = body.get("CreationTimeAfter").and_then(Value::as_f64) {
if creation.map(|c| c >= after) != Some(true) {
return false;
}
}
if let Some(before) = body.get("CreationTimeBefore").and_then(Value::as_f64) {
if creation.map(|c| c <= before) != Some(true) {
return false;
}
}
if let Some(status) = body.get("StatusEquals").and_then(Value::as_str) {
let hit = obj
.map(|o| {
o.iter()
.any(|(k, v)| k.ends_with("Status") && v.as_str() == Some(status))
})
.unwrap_or(false);
if !hit {
return false;
}
}
true
}
pub(super) fn list(
data: Option<&SageMakerData>,
ctx: &Ctx,
meta: &OpMeta,
body: &Map<String, Value>,
) -> AwsResponse {
let mut entries = data
.map(|d| d.list_resource_entries(meta.family))
.unwrap_or_default();
entries.retain(|(id, r)| passes_filters(id, r, body));
list_entries_response(ctx, meta, body, entries)
}
pub(super) fn list_entries_response(
ctx: &Ctx,
meta: &OpMeta,
body: &Map<String, Value>,
entries: Vec<(String, Value)>,
) -> AwsResponse {
let elements: Vec<Value> = if meta.list_scalar {
entries
.iter()
.map(|(id, _)| Value::String(id.clone()))
.collect()
} else {
entries
.iter()
.map(|(id, r)| build_element(ctx, meta, id, r))
.collect()
};
let page_size = body
.get("MaxResults")
.and_then(Value::as_u64)
.map(|n| n as usize)
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_PAGE);
let start = body
.get("NextToken")
.and_then(Value::as_str)
.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 && meta.omembers.iter().any(|(w, _)| *w == "NextToken") {
out.insert("NextToken".to_string(), Value::String(end.to_string()));
}
ok_json(Value::Object(out))
}