harn-cli 0.10.173

CLI for the Harn programming language — run, test, REPL, format, and lint
//! Native view records projected for the Swift host, including serde defaults.

use serde_json::{Map, Value};
use std::collections::BTreeMap;

use super::records::{camel_ident, FieldKind, Record, Target};
use super::schema_records::SchemaRecords;
use super::support::json_string_literal;

pub(super) const SWIFT_ARTIFACT: &str = "HarnRunSessionView.swift";

pub(super) fn schema() -> Value {
    harn_vm::orchestration::run_session_view_schema()
}

pub(super) fn generate_swift(schema: &Value) -> Result<String, String> {
    let definitions = schema["$defs"]
        .as_object()
        .ok_or("view definitions missing")?;
    let names: Vec<_> = definitions
        .iter()
        .filter(|(_, shape)| shape["properties"].is_object())
        .map(|(name, _)| (name.as_str(), format!("Harn{name}")))
        .collect();
    let records = SchemaRecords {
        schema,
        names: &names,
        label: "native run/session view",
        require_all: false,
        metadata: |_, _, shape| {
            if shape == &Value::Bool(true) || shape.as_object().is_some_and(Map::is_empty) {
                return Ok(Some(FieldKind::Json));
            }
            let unsigned = match shape["format"].as_str() {
                Some("uint32") => Some("UInt32"),
                Some("uint64") => Some("UInt64"),
                Some("uint") => Some("UInt"),
                _ => None,
            };
            if let Some(name) = unsigned.filter(|_| shape["type"] == "integer") {
                return Ok(Some(FieldKind::Named(name.into())));
            }
            Ok(match shape["$ref"].as_str() {
                Some("#/$defs/EnvironmentPolicyKind") => {
                    Some(FieldKind::Named("HarnEnvironmentPolicyKind".into()))
                }
                Some("#/$defs/GrantAudience") => Some(FieldKind::Named("HarnGrantAudience".into())),
                Some("#/$defs/FlightValuePolicy") => {
                    Some(FieldKind::Named("HarnFlightValuePolicy".into()))
                }
                _ => None,
            })
        },
    }
    .load_extensible()?;
    let mut out = "// GENERATED by `harn dump-protocol-artifacts` - do not edit by hand.\nimport Foundation\n\n".to_owned();
    for (name, definition) in definitions {
        let values = definition["enum"]
            .as_array()
            .map(|values| values.iter().collect::<Vec<_>>())
            .or_else(|| {
                definition["oneOf"]
                    .as_array()?
                    .iter()
                    .map(|variant| variant.get("const"))
                    .collect::<Option<Vec<_>>>()
            });
        if let Some(values) = values {
            let values = values
                .iter()
                .map(|value| {
                    value
                        .as_str()
                        .map(str::to_owned)
                        .ok_or("view enums must be strings")
                })
                .collect::<Result<Vec<_>, _>>()?;
            out.push_str(&super::swift::swift_enum(&format!("Harn{name}"), &values));
        }
    }
    for (native, public) in &names {
        let record = records
            .iter()
            .find(|record| &record.name == public)
            .unwrap();
        append_defaulted_record(&mut out, record, &definitions[*native]["properties"])?;
    }
    for (name, definition) in [
        ("harnRunViewV1Schema", &definitions["RunView"]),
        ("harnSessionViewV1Schema", &definitions["SessionView"]),
    ] {
        out.push_str(&format!(
            "public let {name} = {}\n",
            definition["properties"]["schema"]["const"]
        ));
        out.push_str(&format!(
            "public let {name}Version: UInt32 = {}\n",
            definition["properties"]["schema_version"]["const"]
        ));
    }
    Ok(out)
}

fn append_defaulted_record(
    out: &mut String,
    record: &Record,
    properties: &Value,
) -> Result<(), String> {
    let mut projected = record.clone();
    let mut body = "\n    public init(from decoder: Decoder) throws {\n        let values = try decoder.container(keyedBy: CodingKeys.self)\n".to_owned();
    let mut defaults = BTreeMap::new();
    for field in &mut projected.fields {
        let property = &properties[field.wire_name.as_ref()];
        let name = camel_ident(&field.wire_name);
        let default = if field.required {
            None
        } else {
            match property.get("default") {
                Some(value) => Some(default_expression(&field.kind, value)?),
                None if matches!(field.kind, FieldKind::Nullable(_)) => Some("nil".into()),
                _ => None,
            }
        };
        if let Some(default) = &default {
            defaults.insert(field.wire_name.to_string(), default.clone());
            field.required = true;
        } else if !field.required {
            return Err(format!(
                "native view field {}.{} has no projected default",
                record.name, field.wire_name
            ));
        }
        let kind = field.kind.type_name(Target::Swift);
        let expression = format!("try values.decode({kind}.self, forKey: .{name})");
        if default.is_some() {
            body.push_str(&format!(
                "        if values.contains(.{name}) {{ {name} = {expression} }}\n"
            ));
        } else {
            body.push_str(&format!("        {name} = {expression}\n"));
        }
    }
    body.push_str("    }\n");
    let required: Vec<_> = record
        .fields
        .iter()
        .filter(|field| field.required)
        .collect();
    if required.is_empty() {
        body.push_str("\n    public init() {}\n");
    } else {
        let parameters = required
            .iter()
            .map(|field| {
                format!(
                    "{}: {}",
                    camel_ident(&field.wire_name),
                    field.kind.type_name(Target::Swift)
                )
            })
            .collect::<Vec<_>>()
            .join(", ");
        body.push_str(&format!("\n    public init({parameters}) {{\n"));
        for field in required {
            let name = camel_ident(&field.wire_name);
            body.push_str(&format!("        self.{name} = {name}\n"));
        }
        body.push_str("    }\n");
    }
    projected.append_swift_with_body(out, &body, &defaults);
    Ok(())
}

fn default_expression(kind: &FieldKind, value: &Value) -> Result<String, String> {
    Ok(match value {
        Value::Null => "nil".into(),
        Value::Bool(_) | Value::Number(_) => value.to_string(),
        Value::String(value) if matches!(kind, FieldKind::Named(_)) => {
            format!(".{}", super::swift::swift_case_name(value))
        }
        Value::String(value) => json_string_literal(value),
        Value::Array(values) if values.is_empty() => "[]".into(),
        Value::Object(_) if matches!(kind, FieldKind::Named(_)) => ".init()".into(),
        Value::Object(values) if values.is_empty() => "[:]".into(),
        _ => return Err(format!("unsupported native default {value} for {kind:?}")),
    })
}