Skip to content

Commit

Permalink
Generate JSON schema using Newtonsoft.Json.Schema
Browse files Browse the repository at this point in the history
  • Loading branch information
erri120 committed Feb 26, 2024
1 parent 383b088 commit 4fd8a9f
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.52" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.3.0" />
<PackageReference Include="System.Management" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" />
Expand Down
33 changes: 33 additions & 0 deletions src/SMAPI.Toolkit/Serialization/JsonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Schema;
using Newtonsoft.Json.Schema.Generation;
using StardewModdingAPI.Toolkit.Serialization.Converters;

namespace StardewModdingAPI.Toolkit.Serialization
Expand All @@ -17,6 +19,13 @@ public class JsonHelper
/// <summary>The JSON settings to use when serializing and deserializing files.</summary>
public JsonSerializerSettings JsonSettings { get; } = JsonHelper.CreateDefaultSettings();

private readonly JSchemaGenerator _schemaGenerator = new();

private readonly JSchemaWriterSettings _schemaWriterSettings = new()
{
Version = SchemaVersion.Draft2019_09,
ReferenceHandling = JSchemaWriterReferenceHandling.Never
};

/*********
** Public methods
Expand Down Expand Up @@ -111,6 +120,30 @@ public void WriteJsonFile<TModel>(string fullPath, TModel model)
File.WriteAllText(fullPath, json);
}

/// <summary>Generate a schema and save to a JSON file.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <param name="fullPath">The absolute file path.</param>
/// <exception cref="InvalidOperationException">The given path is empty or invalid.</exception>
public void WriteJsonSchemaFile<TModel>(string fullPath)
where TModel : class
{
// validate
if (string.IsNullOrWhiteSpace(fullPath))
throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath));

// create directory if needed
string dir = Path.GetDirectoryName(fullPath)!;
if (dir == null)
throw new ArgumentException("The file path is invalid.", nameof(fullPath));
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);

JSchema schema = this._schemaGenerator.Generate(typeof(TModel));
string output = schema.ToString(this._schemaWriterSettings);

File.WriteAllText(fullPath, output);
}

/// <summary>Deserialize JSON text if possible.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <param name="json">The raw JSON text.</param>
Expand Down
14 changes: 12 additions & 2 deletions src/SMAPI/Framework/ModHelpers/DataHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,19 @@ public void WriteJsonFile<TModel>(string path, TModel? data)
File.Delete(path);
}

/// <inheritdoc/>
public void WriteJsonSchemaFile<TModel>(string path, TModel data) where TModel : class
{
if (!PathUtilities.IsSafeRelativePath(path))
throw new InvalidOperationException($"You must call {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteJsonFile)} with a relative path (without directory climbing).");

path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePath(path));
this.JsonHelper.WriteJsonSchemaFile<TModel>(path);
}

/****
** Save file
****/
** Save file
****/
/// <inheritdoc />
public TModel? ReadSaveData<TModel>(string key)
where TModel : class
Expand Down
22 changes: 21 additions & 1 deletion src/SMAPI/Framework/ModHelpers/ModHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Provides simplified APIs for writing mods.</summary>
internal class ModHelper : BaseHelper, IModHelper, IDisposable
{
/*********
** Fields
*********/
/// <summary>Whether to generate config.schema.json files.</summary>
private readonly bool _generateConfigSchemas;

/*********
** Accessors
*********/
Expand Down Expand Up @@ -65,9 +71,10 @@ internal class ModHelper : BaseHelper, IModHelper, IDisposable
/// <param name="reflectionHelper">An API for accessing private game code.</param>
/// <param name="multiplayer">Provides multiplayer utilities.</param>
/// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param>
/// <param name="generateConfigSchemas">Whether to generate config.schema.json files.</param>
/// <exception cref="ArgumentNullException">An argument is null or empty.</exception>
/// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception>
public ModHelper(IModMetadata mod, string modDirectory, Func<SInputState> currentInputState, IModEvents events, IGameContentHelper gameContentHelper, IModContentHelper modContentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper)
public ModHelper(IModMetadata mod, string modDirectory, Func<SInputState> currentInputState, IModEvents events, IGameContentHelper gameContentHelper, IModContentHelper modContentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, bool generateConfigSchemas)
: base(mod)
{
// validate directory
Expand All @@ -89,6 +96,7 @@ public ModHelper(IModMetadata mod, string modDirectory, Func<SInputState> curren
this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer));
this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper));
this.Events = events;
this._generateConfigSchemas = generateConfigSchemas;
}

/****
Expand All @@ -100,6 +108,12 @@ public TConfig ReadConfig<TConfig>()
{
TConfig config = this.Data.ReadJsonFile<TConfig>("config.json") ?? new TConfig();
this.WriteConfig(config); // create file or fill in missing fields

if (this._generateConfigSchemas)
{
this.WriteConfigSchema(config);
}

return config;
}

Expand All @@ -110,6 +124,12 @@ public void WriteConfig<TConfig>(TConfig config)
this.Data.WriteJsonFile("config.json", config);
}

private void WriteConfigSchema<TConfig>(TConfig config)
where TConfig : class, new()
{
this.Data.WriteJsonSchemaFile("config.schema.json", config);
}

/****
** Disposal
****/
Expand Down
2 changes: 1 addition & 1 deletion src/SMAPI/Framework/SCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1953,7 +1953,7 @@ IContentPack[] GetContentPacks()
IModRegistry modRegistryHelper = new ModRegistryHelper(mod, this.ModRegistry, proxyFactory, monitor);
IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(mod, this.Multiplayer);

modHelper = new ModHelper(mod, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
modHelper = new ModHelper(mod, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, this.Settings.GenerateConfigSchemas);
}

// init mod
Expand Down
8 changes: 8 additions & 0 deletions src/SMAPI/IDataHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ public interface IDataHelper
void WriteJsonFile<TModel>(string path, TModel? data)
where TModel : class;

/// <summary>Save the schema of the data to a JSON file in the mod's folder.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="path">The file path relative to the mod folder.</param>
/// <param name="data">The arbitrary data to save.</param>
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
void WriteJsonSchemaFile<TModel>(string path, TModel data)
where TModel : class;

/****
** Save file
****/
Expand Down

0 comments on commit 4fd8a9f

Please sign in to comment.