Code/Resolver/Extractors/JsonUtil.cs
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace Bimp.Resolver.Extractors;

/// <summary>
/// Forgiving accessors for JSON from sites that mix numbers and numeric strings.
/// </summary>
internal static class JsonUtil
{
	public static string Str( this JsonNode node )
	{
		if ( node is not JsonValue v ) return null;
		return v.GetValueKind() switch
		{
			JsonValueKind.String => v.GetValue<string>(),
			JsonValueKind.Number => v.ToJsonString(),
			JsonValueKind.True => "true",
			JsonValueKind.False => "false",
			_ => null,
		};
	}

	public static long Long( this JsonNode node )
	{
		var s = node.Str();
		if ( s is null ) return 0;
		return double.TryParse( s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d ) ? (long)d : 0;
	}

	public static int Int( this JsonNode node ) => (int)node.Long();

	public static float Float( this JsonNode node )
	{
		var s = node.Str();
		return s is not null && float.TryParse( s, NumberStyles.Float, CultureInfo.InvariantCulture, out var f ) ? f : 0;
	}

	public static bool Bool( this JsonNode node ) => node.Str() == "true";

	public static IEnumerable<JsonNode> Items( this JsonNode node )
		=> node is JsonArray a ? a.Where( x => x is not null ) : Enumerable.Empty<JsonNode>();

	/// <summary> Depth first search for the first object property with this name. </summary>
	public static JsonNode Find( this JsonNode node, string name )
	{
		switch ( node )
		{
			case JsonObject o:
				if ( o.TryGetPropertyValue( name, out var direct ) && direct is not null ) return direct;
				foreach ( var (_, child) in o )
				{
					var found = child?.Find( name );
					if ( found is not null ) return found;
				}
				break;
			case JsonArray a:
				foreach ( var child in a )
				{
					var found = child?.Find( name );
					if ( found is not null ) return found;
				}
				break;
		}
		return null;
	}
}