add support for transparent schema errors (#654)

This commit is contained in:
Jesse Plamondon-Willard 2019-08-06 03:19:38 -04:00
parent 74e86de01e
commit 674ceea74e
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
3 changed files with 123 additions and 68 deletions

View File

@ -39,7 +39,8 @@ These changes have not been released yet.
* For the JSON validator:
* Added JSON validator at [json.smapi.io](https://json.smapi.io), which lets you validate a JSON file against predefined mod formats.
* Added support for the `manifest.json` format.
* Added support for Content Patcher's `content.json` format (thanks to TehPers!).
* Added support for the Content Patcher format (thanks to TehPers!).
* Added support for referencing a schema in a JSON Schema-compatible text editor.
* For modders:
* Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised).

View File

@ -4,50 +4,77 @@
and update check API.
## Contents
* [Overview](#overview)
* [Log parser](#log-parser)
* [JSON validator](#json-validator)
* [Web API](#web-api)
* [Log parser](#log-parser)
* [JSON validator](#json-validator)
* [Web API](#web-api)
* [For SMAPI developers](#for-smapi-developers)
* [Local development](#local-development)
* [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk)
## Overview
The `SMAPI.Web` project provides an API and web UI hosted at `*.smapi.io`.
### Log parser
## Log parser
The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are
persisted in a compressed form to Pastebin. The log parser lives at https://log.smapi.io.
### JSON validator
The JSON validator provides a web UI for uploading and sharing JSON files, and validating them
as plain JSON or against a predefined format like `manifest.json` or Content Patcher's
`content.json`. The JSON validator lives at https://json.smapi.io.
## JSON validator
### Overview
The JSON validator provides a web UI for uploading and sharing JSON files, and validating them as
plain JSON or against a predefined format like `manifest.json` or Content Patcher's `content.json`.
The JSON validator lives at https://json.smapi.io.
### Schema file format
Schema files are defined in `wwwroot/schemas` using the [JSON Schema](https://json-schema.org/)
format, with some special properties:
* The root schema may have a `@documentationURL` field, which is the URL to the user-facing
documentation for the format (if any).
* Any part of the schema can define an `@errorMessages` field, which specifies user-friendly errors
which override the auto-generated messages. These can be indexed by error type:
```js
"pattern": "^[a-zA-Z0-9_.-]+\\.dll$",
"@errorMessages": {
"pattern": "Invalid value; must be a filename ending with .dll."
}
```
...or by error type and a regular expression applied to the default message (not recommended
unless the previous form doesn't work, since it's more likely to break in future versions):
```js
"@errorMessages": {
"oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.",
"oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive."
}
```
Error messages can optionally include a `@value` token, which will be replaced with the error's
value field (which is usually the original field value).
format. The JSON validator UI recognises a superset of the standard fields to change output:
You can also reference these schemas in your JSON file directly using the `$schema` field, for
<dl>
<dt>Documentation URL</dt>
<dd>
The root schema may have a `@documentationURL` field, which is a web URL for the user
documentation:
```js
"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest"
```
If present, this is shown in the JSON validator UI.
</dd>
<dt>Error messages</dt>
<dd>
Any part of the schema can define an `@errorMessages` field, which overrides matching schema
errors. You can override by error code (recommended), or by error type and a regex pattern matched
against the error message (more fragile):
```js
// by error type
"pattern": "^[a-zA-Z0-9_.-]+\\.dll$",
"@errorMessages": {
"pattern": "Invalid value; must be a filename ending with .dll."
}
```
```js
// by error type + message pattern
"@errorMessages": {
"oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.",
"oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive."
}
```
Error messages may contain special tokens:
* `@value` is replaced with the error's value field (which is usually the original field value, but
not always).
* If the validation error has exactly one sub-error and the message is set to `$transparent`, the
sub-error will be displayed instead. (The sub-error itself may be set to `$transparent`, etc.)
Caveats:
* To override an error from a `then` block, the `@errorMessages` must be inside the `then` block
instead of adjacent.
</dd>
</dl>
### Using a schema file directly
You can reference the validator schemas in your JSON file directly using the `$schema` field, for
text editors that support schema validation. For example:
```js
{
@ -64,11 +91,13 @@ format | schema URL
[SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json
[Content Patcher `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json
### Web API
## Web API
### Overview
SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a
`{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly
accessible but not officially released; it may change at any time.
### `/mods` endpoint
The API has one `/mods` endpoint. This provides mod info, including official versions and URLs
(from Chucklefish, GitHub, or Nexus), unofficial versions from the wiki, and optional mod metadata
from the wiki and SMAPI's internal data. This is used by SMAPI to perform update checks, and by

View File

@ -124,7 +124,7 @@ namespace StardewModdingAPI.Web.Controllers
// validate JSON
parsed.IsValid(schema, out IList<ValidationError> rawErrors);
var errors = rawErrors
.Select(error => new JsonValidatorErrorModel(error.LineNumber, error.Path, this.GetFlattenedError(error), error.ErrorType))
.Select(this.GetErrorModel)
.ToArray();
return this.View("Index", result.AddErrors(errors));
}
@ -175,35 +175,6 @@ namespace StardewModdingAPI.Web.Controllers
return response;
}
/// <summary>Get a flattened, human-readable message representing a schema validation error.</summary>
/// <param name="error">The error to represent.</param>
/// <param name="indent">The indentation level to apply for inner errors.</param>
private string GetFlattenedError(ValidationError error, int indent = 0)
{
// get override error
string message = this.GetOverrideError(error);
if (message != null)
return message;
// get friendly representation of main error
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;
case ErrorType.Required:
message = $"Missing required fields: {string.Join(", ", (List<string>)error.Value)}.";
break;
}
// add inner errors
foreach (ValidationError childError in error.ChildErrors)
message += "\n" + "".PadLeft(indent * 2, ' ') + $"==> {childError.Path}: " + this.GetFlattenedError(childError, indent + 1);
return message;
}
/// <summary>Get a normalised schema name, or the <see cref="DefaultSchemaID"/> if blank.</summary>
/// <param name="schemaName">The raw schema name to normalise.</param>
private string NormaliseSchemaName(string schemaName)
@ -234,6 +205,60 @@ namespace StardewModdingAPI.Web.Controllers
return null;
}
/// <summary>Get a flattened representation representation of a schema validation error and any child errors.</summary>
/// <param name="error">The error to represent.</param>
private JsonValidatorErrorModel GetErrorModel(ValidationError error)
{
// skip through transparent errors
while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1)
error = error.ChildErrors[0];
// get message
string message = this.GetOverrideError(error);
if (message == null)
message = this.FlattenErrorMessage(error);
// build model
return new JsonValidatorErrorModel(error.LineNumber, error.Path, message, error.ErrorType);
}
/// <summary>Get a flattened, human-readable message for a schema validation error and any child errors.</summary>
/// <param name="error">The error to represent.</param>
/// <param name="indent">The indentation level to apply for inner errors.</param>
private string FlattenErrorMessage(ValidationError error, int indent = 0)
{
// get override
string message = this.GetOverrideError(error);
if (message != null)
return message;
// skip through transparent errors
while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1)
error = error.ChildErrors[0];
// get friendly representation of main error
message = error.Message;
switch (error.ErrorType)
{
case ErrorType.Const:
message = $"Invalid value. Found '{error.Value}', but expected '{error.Schema.Const}'.";
break;
case ErrorType.Enum:
message = $"Invalid value. Found '{error.Value}', but expected one of '{string.Join("', '", error.Schema.Enum)}'.";
break;
case ErrorType.Required:
message = $"Missing required fields: {string.Join(", ", (List<string>)error.Value)}.";
break;
}
// add inner errors
foreach (ValidationError childError in error.ChildErrors)
message += "\n" + "".PadLeft(indent * 2, ' ') + $"==> {childError.Path}: " + this.FlattenErrorMessage(childError, indent + 1);
return message;
}
/// <summary>Get an override error from the JSON schema, if any.</summary>
/// <param name="error">The schema validation error.</param>
private string GetOverrideError(ValidationError error)
@ -254,12 +279,12 @@ namespace StardewModdingAPI.Web.Controllers
string[] parts = pair.Key.Split(':', 2);
if (parts[0].Equals(error.ErrorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(error.Message, parts[1]))
return pair.Value;
return pair.Value?.Trim();
}
// match by type
if (errors.TryGetValue(error.ErrorType.ToString(), out string message))
return message;
return message?.Trim();
return null;
}