// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace EpicGames.Core
{
///
/// Interface used to write JSON schema types. This is abstracted to allow multiple passes over the document structure, in order to optimize multiple references to the same type definition.
///
public interface IJsonSchemaWriter
{
///
void WriteStartObject();
///
void WriteStartObject(string name);
///
void WriteEndObject();
///
void WriteStartArray();
///
void WriteStartArray(string name);
///
void WriteEndArray();
///
void WriteBoolean(string name, bool value);
///
void WriteString(string key, string value);
///
void WriteStringValue(string name);
///
/// Serialize a type to JSON
///
///
void WriteType(JsonSchemaType type);
}
///
/// Implementation of a JSON schema. Implements draft 04 (latest supported by Visual Studio 2019).
///
public class JsonSchema
{
class JsonTypeRefCollector : IJsonSchemaWriter
{
///
/// Reference counts for each type (max of 2)
///
public Dictionary TypeRefCount { get; } = [];
///
public void WriteBoolean(string name, bool value) { }
///
public void WriteString(string key, string value) { }
///
public void WriteStringValue(string name) { }
///
public void WriteStartObject() { }
///
public void WriteStartObject(string name) { }
///
public void WriteEndObject() { }
///
public void WriteStartArray() { }
///
public void WriteStartArray(string name) { }
///
public void WriteEndArray() { }
///
public void WriteType(JsonSchemaType type)
{
if (type is not JsonSchemaPrimitiveType)
{
TypeRefCount.TryGetValue(type, out int refCount);
if (refCount < 2)
{
TypeRefCount[type] = ++refCount;
}
if (refCount < 2)
{
type.Write(this);
}
}
}
}
///
/// Implementation of
///
class JsonSchemaWriter : IJsonSchemaWriter
{
///
/// Raw Json output
///
readonly Utf8JsonWriter _jsonWriter;
///
/// Mapping of type to definition name
///
readonly Dictionary _typeToDefinition;
///
/// Constructor
///
///
///
public JsonSchemaWriter(Utf8JsonWriter writer, Dictionary typeToDefinition)
{
_jsonWriter = writer;
_typeToDefinition = typeToDefinition;
}
///
public void WriteBoolean(string name, bool value) => _jsonWriter.WriteBoolean(name, value);
///
public void WriteString(string key, string value) => _jsonWriter.WriteString(key, value);
///
public void WriteStringValue(string name) => _jsonWriter.WriteStringValue(name);
///
public void WriteStartObject() => _jsonWriter.WriteStartObject();
///
public void WriteStartObject(string name) => _jsonWriter.WriteStartObject(name);
///
public void WriteEndObject() => _jsonWriter.WriteEndObject();
///
public void WriteStartArray() => _jsonWriter.WriteStartArray();
///
public void WriteStartArray(string name) => _jsonWriter.WriteStartArray(name);
///
public void WriteEndArray() => _jsonWriter.WriteEndArray();
///
/// Writes a type, either inline or as a reference to a definition elsewhere
///
///
public void WriteType(JsonSchemaType type)
{
if (_typeToDefinition.TryGetValue(type, out string? definition))
{
_jsonWriter.WriteString("$ref", $"#/definitions/{definition}");
}
else
{
type.Write(this);
}
}
}
///
/// Identifier for the schema
///
public string? Id { get; set; }
///
/// The root schema type
///
public JsonSchemaType RootType { get; set; }
///
/// Constructor
///
/// Id for the schema
///
public JsonSchema(string? id, JsonSchemaType rootType)
{
Id = id;
RootType = rootType;
}
///
/// Write this schema to a byte array
///
///
public void Write(Utf8JsonWriter writer)
{
// Determine reference counts for each type. Any type referenced at least twice will be split off into a separate definition.
JsonTypeRefCollector refCollector = new JsonTypeRefCollector();
refCollector.WriteType(RootType);
// Assign names to each type definition
HashSet definitionNames = [];
Dictionary typeToDefinition = [];
foreach ((JsonSchemaType type, int refCount) in refCollector.TypeRefCount)
{
if (refCount > 1)
{
string baseName = type.Name ?? "unnamed";
string name = baseName;
for (int idx = 1; !definitionNames.Add(name); idx++)
{
name = $"{baseName}{idx}";
}
typeToDefinition[type] = name;
}
}
// Write the schema
writer.WriteStartObject();
writer.WriteString("$schema", "http://json-schema.org/draft-04/schema#");
if (Id != null)
{
writer.WriteString("$id", Id);
}
JsonSchemaWriter schemaWriter = new JsonSchemaWriter(writer, typeToDefinition);
RootType.Write(schemaWriter);
if (typeToDefinition.Count > 0)
{
writer.WriteStartObject("definitions");
foreach ((JsonSchemaType type, string refName) in typeToDefinition)
{
writer.WriteStartObject(refName);
type.Write(schemaWriter);
writer.WriteEndObject();
}
writer.WriteEndObject();
}
writer.WriteEndObject();
}
///
/// Write this schema to a stream
///
/// The output stream
public void Write(Stream stream)
{
using (Utf8JsonWriter writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
{
Write(writer);
}
}
///
/// Writes this schema to a file
///
/// The output file
public void Write(FileReference file)
{
using (FileStream stream = FileReference.Open(file, FileMode.Create))
{
Write(stream);
}
}
///
/// Constructs a Json schema from a type
///
/// The type to construct from
/// Reads documentation for requested types
/// New schema object
public static JsonSchema FromType(Type type, XmlDocReader? xmlDocReader)
{
JsonSchemaFactory factory = new JsonSchemaFactory(xmlDocReader);
return factory.CreateSchema(type);
}
///
/// Create a Json schema (or retrieve a cached schema)
///
public static JsonSchema CreateSchema(Type type, XmlDocReader? xmlDocReader)
{
JsonSchemaFactory factory = new JsonSchemaFactory(xmlDocReader);
return factory.CreateSchema(type);
}
}
}