tools/code/common/Json.cs (165 lines of code) (raw):

using LanguageExt; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; namespace common; public static class JsonValueExtensions { public static Either<string, string> TryAsString(this JsonValue? jsonValue) => jsonValue is null ? Either<string, string>.Left("JSON value is null.") : jsonValue.TryGetValue<string>(out var stringValue) ? Either<string, string>.Right(stringValue) : Either<string, string>.Left("JSON value is not a string."); public static Either<string, Uri> TryAsAbsoluteUri(this JsonValue? jsonValue) => jsonValue.TryAsString() .Bind(uriString => Uri.TryCreate(uriString, UriKind.Absolute, out var uri) ? Either<string, Uri>.Right(uri) : $"JSON value '{uriString}' is not a valid absolute URI."); public static Either<string, int> TryAsInt(this JsonValue? jsonValue) => jsonValue is null ? "JSON value is null." : jsonValue.TryGetValue<int>(out var intValue) || (jsonValue.TryGetValue<string>(out var stringValue) && int.TryParse(stringValue, out intValue)) ? intValue : "JSON value is not an integer."; public static Either<string, bool> TryAsBool(this JsonValue? jsonValue) => jsonValue is null ? "JSON value is null." : jsonValue.TryGetValue<bool>(out var boolValue) || (jsonValue.TryGetValue<string>(out var stringValue) && bool.TryParse(stringValue, out boolValue)) ? boolValue : "JSON value is not a boolean."; } public static class JsonNodeExtensions { public static Either<string, JsonObject> TryAsJsonObject(this JsonNode? node) => node is JsonObject jsonObject ? jsonObject : "Node is not a JSON object."; public static Either<string, JsonArray> TryAsJsonArray(this JsonNode? node) => node is JsonArray jsonArray ? jsonArray : "Node is not a JSON array."; public static Either<string, JsonValue> TryAsJsonValue(this JsonNode? node) => node is JsonValue jsonValue ? jsonValue : "Node is not a JSON value."; public static Either<string, string> TryAsString(this JsonNode? node) => node.TryAsJsonValue() .Bind(jsonValue => jsonValue.TryAsString()); public static Either<string, Uri> TryAsAbsoluteUri(this JsonNode? node) => node.TryAsJsonValue() .Bind(jsonValue => jsonValue.TryAsAbsoluteUri()); public static Either<string, int> TryAsInt(this JsonNode? node) => node.TryAsJsonValue() .Bind(jsonValue => jsonValue.TryAsInt()); public static Either<string, bool> TryAsBool(this JsonNode? node) => node.TryAsJsonValue() .Bind(jsonValue => jsonValue.TryAsBool()); } public static class JsonArrayExtensions { public static ImmutableArray<JsonObject> PickJsonObjects(this JsonArray jsonArray) => jsonArray.Choose(node => node.TryAsJsonObject().ToOption()) .ToImmutableArray(); public static ImmutableArray<string> PickStrings(this JsonArray jsonArray) => jsonArray.Choose(node => node.TryAsString().ToOption()) .ToImmutableArray(); public static JsonArray ToJsonArray(this IEnumerable<JsonNode> nodes) => new(nodes.ToArray()); } public static class JsonObjectExtensions { public static JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }; public static JsonNode GetProperty(this JsonObject? jsonObject, string propertyName) => jsonObject.TryGetProperty(propertyName) .IfLeftThrow(); public static Either<string, JsonNode> TryGetProperty(this JsonObject? jsonObject, string propertyName) => jsonObject is null ? "JSON object is null." : jsonObject.TryGetPropertyValue(propertyName, out var property) ? property is null ? $"Property '{propertyName}' is null." : property : $"Property '{propertyName}' is missing."; private static T IfLeftThrow<T>(this Either<string, T> either) => either.IfLeft(error => throw new JsonException(error)); public static JsonObject GetJsonObjectProperty(this JsonObject? jsonObject, string propertyName) => jsonObject.TryGetJsonObjectProperty(propertyName) .IfLeftThrow(); public static Either<string, JsonObject> TryGetJsonObjectProperty(this JsonObject? jsonObject, string propertyName) => jsonObject.TryGetProperty(propertyName) .Bind(JsonNodeExtensions.TryAsJsonObject) .BindPropertyError(propertyName); private static Either<string, T> BindPropertyError<T>(this Either<string, T> either, string propertyName) => either.MapLeft(error => $"Property '{propertyName}' is invalid. {error}"); public static Either<string, JsonArray> TryGetJsonArrayProperty(this JsonObject? jsonObject, string propertyName) => jsonObject.TryGetProperty(propertyName) .Bind(JsonNodeExtensions.TryAsJsonArray) .BindPropertyError(propertyName); public static Uri GetAbsoluteUriProperty(this JsonObject jsonObject, string propertyName) => jsonObject.TryGetAbsoluteUriProperty(propertyName) .IfLeftThrow(); public static Either<string, Uri> TryGetAbsoluteUriProperty(this JsonObject? jsonObject, string propertyName) => jsonObject.TryGetProperty(propertyName) .Bind(JsonNodeExtensions.TryAsAbsoluteUri) .BindPropertyError(propertyName); public static Either<string, string> TryGetStringProperty(this JsonObject? jsonObject, string propertyName) => jsonObject.TryGetProperty(propertyName) .Bind(JsonNodeExtensions.TryAsString) .BindPropertyError(propertyName); public static string GetNonEmptyOrWhiteSpaceStringProperty(this JsonObject jsonObject, string propertyName) => jsonObject.TryGetNonEmptyOrWhiteSpaceStringProperty(propertyName) .IfLeftThrow(); public static Either<string, string> TryGetNonEmptyOrWhiteSpaceStringProperty(this JsonObject? jsonObject, string propertyName) => jsonObject.TryGetProperty(propertyName) .Bind(JsonNodeExtensions.TryAsString) .Bind(value => string.IsNullOrWhiteSpace(value) ? Either<string, string>.Left($"Property '{propertyName}' is empty or whitespace.") : Either<string, string>.Right(value)) .BindPropertyError(propertyName); public static string GetStringProperty(this JsonObject jsonObject, string propertyName) => jsonObject.TryGetStringProperty(propertyName) .IfLeftThrow(); public static int GetIntProperty(this JsonObject jsonObject, string propertyName) => jsonObject.TryGetIntProperty(propertyName) .IfLeftThrow(); public static Either<string, int> TryGetIntProperty(this JsonObject? jsonObject, string propertyName) => jsonObject.TryGetProperty(propertyName) .Bind(JsonNodeExtensions.TryAsInt) .BindPropertyError(propertyName); public static bool GetBoolProperty(this JsonObject jsonObject, string propertyName) => jsonObject.TryGetBoolProperty(propertyName) .IfLeftThrow(); public static Either<string, bool> TryGetBoolProperty(this JsonObject? jsonObject, string propertyName) => jsonObject.TryGetProperty(propertyName) .Bind(JsonNodeExtensions.TryAsBool) .BindPropertyError(propertyName); public static JsonObject Parse<T>(T obj) => TryParse(obj) .IfLeft(() => throw new JsonException($"Could not parse {typeof(T).Name} as a JSON object.")); public static Either<string, JsonObject> TryParse<T>(T obj) => JsonSerializer.SerializeToNode(obj, SerializerOptions) .TryAsJsonObject(); [return: NotNullIfNotNull(nameof(jsonObject))] public static JsonObject? SetProperty(this JsonObject? jsonObject, string propertyName, JsonNode? jsonNode) { if (jsonObject is null) { return null; } else { jsonObject[propertyName] = jsonNode; return jsonObject; } } }