add support for transparent schema errors (#654)
This commit is contained in:
parent
74e86de01e
commit
674ceea74e
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue