using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using StardewModdingAPI.Web.Framework;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
using StardewModdingAPI.Web.Framework.Compression;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.ViewModels.JsonValidator;
namespace StardewModdingAPI.Web.Controllers
{
/// Provides a web UI for validating JSON schemas.
internal class JsonValidatorController : Controller
{
/*********
** Fields
*********/
/// The site config settings.
private readonly SiteConfig Config;
/// The underlying Pastebin client.
private readonly IPastebinClient Pastebin;
/// The underlying text compression helper.
private readonly IGzipHelper GzipHelper;
/// The section URL for the schema validator.
private string SectionUrl => this.Config.JsonValidatorUrl;
/// The supported JSON schemas (names indexed by ID).
private readonly IDictionary SchemaFormats = new Dictionary
{
["none"] = "None",
["manifest"] = "Manifest"
};
/// The schema ID to use if none was specified.
private string DefaultSchemaID = "manifest";
/*********
** Public methods
*********/
/***
** Constructor
***/
/// Construct an instance.
/// The context config settings.
/// The Pastebin API client.
/// The underlying text compression helper.
public JsonValidatorController(IOptions siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper)
{
this.Config = siteConfig.Value;
this.Pastebin = pastebin;
this.GzipHelper = gzipHelper;
}
/***
** Web UI
***/
/// Render the schema validator UI.
/// The schema name with which to validate the JSON.
/// The paste ID.
[HttpGet]
[Route("json")]
[Route("json/{schemaName}")]
[Route("json/{schemaName}/{id}")]
public async Task Index(string schemaName = null, string id = null)
{
schemaName = this.NormaliseSchemaName(schemaName);
var result = new JsonValidatorModel(this.SectionUrl, id, schemaName, this.SchemaFormats);
if (string.IsNullOrWhiteSpace(id))
return this.View("Index", result);
// fetch raw JSON
PasteInfo paste = await this.GetAsync(id);
if (string.IsNullOrWhiteSpace(paste.Content))
return this.View("Index", result.SetUploadError("The JSON file seems to be empty."));
result.SetContent(paste.Content);
// parse JSON
JToken parsed;
try
{
parsed = JToken.Parse(paste.Content);
}
catch (JsonReaderException ex)
{
return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path, ex.Message)));
}
// format JSON
result.SetContent(parsed.ToString(Formatting.Indented));
// skip if no schema selected
if (schemaName == "none")
return this.View("Index", result);
// load schema
JSchema schema;
{
FileInfo schemaFile = this.FindSchemaFile(schemaName);
if (schemaFile == null)
return this.View("Index", result.SetParseError($"Invalid schema '{schemaName}'."));
schema = JSchema.Parse(System.IO.File.ReadAllText(schemaFile.FullName));
}
// validate JSON
parsed.IsValid(schema, out IList rawErrors);
var errors = rawErrors
.Select(error => new JsonValidatorErrorModel(error.LineNumber, error.Path, this.GetFlattenedError(error)))
.ToArray();
return this.View("Index", result.AddErrors(errors));
}
/***
** JSON
***/
/// Save raw JSON data.
[HttpPost, AllowLargePosts]
[Route("json")]
public async Task PostAsync(JsonValidatorRequestModel request)
{
if (request == null)
return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, null, this.SchemaFormats).SetUploadError("The request seems to be invalid."));
// normalise schema name
string schemaName = this.NormaliseSchemaName(request.SchemaName);
// get raw log text
string input = request.Content;
if (string.IsNullOrWhiteSpace(input))
return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, schemaName, this.SchemaFormats).SetUploadError("The JSON file seems to be empty."));
// upload log
input = this.GzipHelper.CompressString(input);
SavePasteResult result = await this.Pastebin.PostAsync($"JSON validator {DateTime.UtcNow:s}", input);
// handle errors
if (!result.Success)
return this.View("Index", new JsonValidatorModel(this.SectionUrl, result.ID, schemaName, this.SchemaFormats).SetUploadError($"Pastebin error: {result.Error ?? "unknown error"}"));
// redirect to view
UriBuilder uri = new UriBuilder(new Uri(this.SectionUrl));
uri.Path = $"{uri.Path.TrimEnd('/')}/{schemaName}/{result.ID}";
return this.Redirect(uri.Uri.ToString());
}
/*********
** Private methods
*********/
/// Fetch raw text from Pastebin.
/// The Pastebin paste ID.
private async Task GetAsync(string id)
{
PasteInfo response = await this.Pastebin.GetAsync(id);
response.Content = this.GzipHelper.DecompressString(response.Content);
return response;
}
/// Get a flattened, human-readable message representing a schema validation error.
/// The error to represent.
/// The indentation level to apply for inner errors.
private string GetFlattenedError(ValidationError error, int indent = 0)
{
// get friendly representation of main error
string message = error.Message;
switch (error.ErrorType)
{
case ErrorType.Enum:
message = $"Invalid value. Found '{error.Value}', but expected one of '{string.Join("', '", error.Schema.Enum)}'.";
break;
}
// add inner errors
foreach (ValidationError childError in error.ChildErrors)
message += "\n" + "".PadLeft(indent * 2, ' ') + $"==> {childError.Path}: " + this.GetFlattenedError(childError, indent + 1);
return message;
}
/// Get a normalised schema name, or the if blank.
/// The raw schema name to normalise.
private string NormaliseSchemaName(string schemaName)
{
schemaName = schemaName?.Trim().ToLower();
return !string.IsNullOrWhiteSpace(schemaName)
? schemaName
: this.DefaultSchemaID;
}
/// Get the schema file given its unique ID.
/// The schema ID.
private FileInfo FindSchemaFile(string id)
{
// normalise ID
id = id?.Trim().ToLower();
if (string.IsNullOrWhiteSpace(id))
return null;
// get matching file
DirectoryInfo schemaDir = new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "schemas"));
foreach (FileInfo file in schemaDir.EnumerateFiles("*.json"))
{
if (file.Name.Equals($"{id}.json"))
return file;
}
return null;
}
}
}