using System; using System.Globalization; using Newtonsoft.Json.Linq; namespace MCPForUnity.Editor.Helpers { /// /// Utility class for coercing JSON parameter values to strongly-typed values. /// Handles various input formats (strings, numbers, booleans) gracefully. /// public static class ParamCoercion { /// /// Coerces a JToken to an integer value, handling strings and floats. /// /// The JSON token to coerce /// Default value if coercion fails /// The coerced integer value or default public static int CoerceInt(JToken token, int defaultValue) { if (token == null || token.Type == JTokenType.Null) return defaultValue; try { if (token.Type == JTokenType.Integer) return token.Value(); var s = token.ToString().Trim(); if (s.Length == 0) return defaultValue; if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)) return i; if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)) return (int)d; } catch { // Swallow and return default } return defaultValue; } /// /// Coerces a JToken to a boolean value, handling strings like "true", "1", etc. /// /// The JSON token to coerce /// Default value if coercion fails /// The coerced boolean value or default public static bool CoerceBool(JToken token, bool defaultValue) { if (token == null || token.Type == JTokenType.Null) return defaultValue; try { if (token.Type == JTokenType.Boolean) return token.Value(); var s = token.ToString().Trim().ToLowerInvariant(); if (s.Length == 0) return defaultValue; if (bool.TryParse(s, out var b)) return b; if (s == "1" || s == "yes" || s == "on") return true; if (s == "0" || s == "no" || s == "off") return false; } catch { // Swallow and return default } return defaultValue; } /// /// Coerces a JToken to a float value, handling strings and integers. /// /// The JSON token to coerce /// Default value if coercion fails /// The coerced float value or default public static float CoerceFloat(JToken token, float defaultValue) { if (token == null || token.Type == JTokenType.Null) return defaultValue; try { if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer) return token.Value(); var s = token.ToString().Trim(); if (s.Length == 0) return defaultValue; if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var f)) return f; } catch { // Swallow and return default } return defaultValue; } /// /// Coerces a JToken to a string value, with null handling. /// /// The JSON token to coerce /// Default value if null or empty /// The string value or default public static string CoerceString(JToken token, string defaultValue = null) { if (token == null || token.Type == JTokenType.Null) return defaultValue; var s = token.ToString(); return string.IsNullOrEmpty(s) ? defaultValue : s; } /// /// Coerces a JToken to an enum value, handling strings. /// /// The enum type /// The JSON token to coerce /// Default value if coercion fails /// The coerced enum value or default public static T CoerceEnum(JToken token, T defaultValue) where T : struct, Enum { if (token == null || token.Type == JTokenType.Null) return defaultValue; try { var s = token.ToString().Trim(); if (s.Length == 0) return defaultValue; if (Enum.TryParse(s, ignoreCase: true, out var result)) return result; } catch { // Swallow and return default } return defaultValue; } /// /// Checks if a JToken represents a numeric value (integer or float). /// Useful for validating JSON values before parsing. /// /// The JSON token to check /// True if the token is an integer or float, false otherwise public static bool IsNumericToken(JToken token) { return token != null && (token.Type == JTokenType.Integer || token.Type == JTokenType.Float); } /// /// Validates that an optional field in a JObject is numeric if present. /// Used for dry-run validation of complex type formats. /// /// The JSON object containing the field /// The name of the field to validate /// Output error message if validation fails /// True if the field is absent, null, or numeric; false if present but non-numeric public static bool ValidateNumericField(JObject obj, string fieldName, out string error) { error = null; var token = obj[fieldName]; if (token == null || token.Type == JTokenType.Null) { return true; // Field not present, valid (will use default) } if (!IsNumericToken(token)) { error = $"must be a number, got {token.Type}"; return false; } return true; } /// /// Validates that an optional field in a JObject is an integer if present. /// Used for dry-run validation of complex type formats. /// /// The JSON object containing the field /// The name of the field to validate /// Output error message if validation fails /// True if the field is absent, null, or integer; false if present but non-integer public static bool ValidateIntegerField(JObject obj, string fieldName, out string error) { error = null; var token = obj[fieldName]; if (token == null || token.Type == JTokenType.Null) { return true; // Field not present, valid } if (token.Type != JTokenType.Integer) { error = $"must be an integer, got {token.Type}"; return false; } return true; } /// /// Normalizes a property name by removing separators and converting to camelCase. /// Handles common naming variations from LLMs and humans. /// Examples: /// "Use Gravity" → "useGravity" /// "is_kinematic" → "isKinematic" /// "max-angular-velocity" → "maxAngularVelocity" /// "Angular Drag" → "angularDrag" /// /// The property name to normalize /// The normalized camelCase property name public static string NormalizePropertyName(string input) { if (string.IsNullOrEmpty(input)) return input; // Split on common separators: space, underscore, dash var parts = input.Split(new[] { ' ', '_', '-' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) return input; // First word is lowercase, subsequent words are Title case (camelCase) var sb = new System.Text.StringBuilder(); for (int i = 0; i < parts.Length; i++) { string part = parts[i]; if (i == 0) { // First word: all lowercase sb.Append(part.ToLowerInvariant()); } else { // Subsequent words: capitalize first letter, lowercase rest sb.Append(char.ToUpperInvariant(part[0])); if (part.Length > 1) sb.Append(part.Substring(1).ToLowerInvariant()); } } return sb.ToString(); } } }