File: TypeScriptApiExportWriter.cs
Web Access
Project: src\src\Aspire.Hosting.CodeGeneration.TypeScript\Aspire.Hosting.CodeGeneration.TypeScript.csproj (Aspire.Hosting.CodeGeneration.TypeScript)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json;
using System.Text.Json.Nodes;
 
namespace Aspire.Hosting.CodeGeneration.TypeScript;
 
/// <summary>
/// Serializes a <see cref="TypeScriptApiModel"/> into the canonical schema version 1 export document.
/// </summary>
/// <remarks>
/// The document is written by hand rather than through reflection-based serialization because the
/// shape is a published contract that documentation sites bind to. Writing it explicitly makes the
/// contract reviewable in one place, keeps property order stable so exports diff cleanly between SDK
/// versions, and drops empty collections and null strings so a version bump only shows real API
/// changes.
/// </remarks>
internal static class TypeScriptApiExportWriter
{
    public static JsonObject Write(TypeScriptApiModel model)
    {
        ArgumentNullException.ThrowIfNull(model);
 
        var modules = new JsonArray();
        foreach (var module in model.Modules)
        {
            modules.Add((JsonNode)WriteModule(module));
        }
 
        var declarations = new JsonArray();
        foreach (var declaration in model.Declarations)
        {
            declarations.Add((JsonNode)new JsonObject
            {
                ["id"] = declaration.Id,
                ["owningAssembly"] = declaration.OwningAssemblyName,
                ["content"] = declaration.Content
            });
        }
 
        return new JsonObject
        {
            ["schemaVersion"] = model.SchemaVersion,
            ["language"] = model.Language,
            ["generator"] = new JsonObject
            {
                ["name"] = model.Generator.Name,
                ["version"] = model.Generator.Version
            },
            ["package"] = new JsonObject
            {
                ["name"] = model.Package.Name,
                ["version"] = model.Package.Version
            },
            ["modules"] = modules,
            ["declarations"] = declarations
        };
    }
 
    /// <summary>
    /// Serializes the export document to UTF-8 JSON text.
    /// </summary>
    /// <param name="model">The model to serialize.</param>
    /// <param name="indented">
    /// When <see langword="true"/>, writes human-readable JSON. Machine consumers use the compact
    /// form so the document is a single line on stdout.
    /// </param>
    public static string WriteToJson(TypeScriptApiModel model, bool indented = false)
        => Write(model).ToJsonString(new JsonSerializerOptions { WriteIndented = indented });
 
    private static JsonObject WriteModule(TypeScriptApiModule module)
    {
        var items = new JsonArray();
        foreach (var item in module.Items)
        {
            items.Add((JsonNode)WriteItem(item));
        }
 
        var json = new JsonObject { ["name"] = module.Name };
        AddIfPresent(json, "summary", module.Summary);
        json["items"] = items;
        return json;
    }
 
    private static JsonObject WriteItem(TypeScriptApiItem item)
    {
        var json = new JsonObject
        {
            ["id"] = item.Id,
            ["kind"] = ToKindString(item.Kind),
            ["name"] = item.Name,
            ["typeId"] = item.TypeId,
            ["owningAssembly"] = item.OwningAssemblyName,
            ["declaration"] = item.Declaration
        };
 
        AddIfPresent(json, "summary", item.Summary);
        AddIfPresent(json, "remarks", item.Remarks);
        AddIfPresent(json, "examples", item.Examples);
        AddIfPresent(json, "extends", item.Extends);
 
        if (item.Members.Count > 0)
        {
            var members = new JsonArray();
            foreach (var member in item.Members)
            {
                members.Add((JsonNode)WriteMember(member));
            }
 
            json["members"] = members;
        }
 
        return json;
    }
 
    private static JsonObject WriteMember(TypeScriptApiMember member)
    {
        var json = new JsonObject
        {
            ["id"] = member.Id,
            ["kind"] = ToKindString(member.Kind),
            ["name"] = member.Name,
            ["declaration"] = member.Declaration
        };
 
        AddIfPresent(json, "capabilityId", member.CapabilityId);
        AddIfPresent(json, "returnType", member.ReturnType);
        AddIfPresent(json, "summary", member.Summary);
        AddIfPresent(json, "remarks", member.Remarks);
        AddIfPresent(json, "examples", member.Examples);
        if (member.DeprecationMessage is not null)
        {
            json["deprecated"] = member.DeprecationMessage;
        }
 
        if (member.Parameters.Count > 0)
        {
            var parameters = new JsonArray();
            foreach (var parameter in member.Parameters)
            {
                var parameterJson = new JsonObject
                {
                    ["name"] = parameter.Name,
                    ["type"] = parameter.DeclaredType,
                    ["optional"] = parameter.IsOptional
                };
 
                AddIfPresent(parameterJson, "summary", parameter.Summary);
                parameters.Add((JsonNode)parameterJson);
            }
 
            json["parameters"] = parameters;
        }
 
        return json;
    }
 
    private static string ToKindString(TypeScriptApiItemKind kind) => kind switch
    {
        TypeScriptApiItemKind.Interface => "interface",
        TypeScriptApiItemKind.Enum => "enum",
        TypeScriptApiItemKind.Dto => "dto",
        TypeScriptApiItemKind.Options => "options",
        TypeScriptApiItemKind.Namespace => "namespace",
        TypeScriptApiItemKind.Constant => "constant",
        TypeScriptApiItemKind.Augmentation => "augmentation",
        TypeScriptApiItemKind.Method => "method",
        TypeScriptApiItemKind.Property => "property",
        _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown API item kind.")
    };
 
    private static void AddIfPresent(JsonObject json, string name, string? value)
    {
        if (!string.IsNullOrEmpty(value))
        {
            json[name] = value;
        }
    }
 
    private static void AddIfPresent(JsonObject json, string name, IReadOnlyList<string> values)
    {
        if (values.Count == 0)
        {
            return;
        }
 
        var array = new JsonArray();
        foreach (var value in values)
        {
            array.Add((JsonNode)JsonValue.Create(value));
        }
 
        json[name] = array;
    }
}