Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate template.json files against JSON Schema #27

Open
khalidabuhakmeh opened this issue May 27, 2021 · 2 comments
Open

Validate template.json files against JSON Schema #27

khalidabuhakmeh opened this issue May 27, 2021 · 2 comments

Comments

@khalidabuhakmeh
Copy link

khalidabuhakmeh commented May 27, 2021

All template.json files in a template should be validated and pass validation against http://json.schemastore.org/template. Currently, there are some validation issues across templates, ranging from the incorrect casing of properties, comments, and more.

Demo Code - Validate Against JSON Schema

This sample uses the following NuGet packages:

  • Newtonsoft.Json
  • Newtonsoft.Json.Schema
  • Spectre.Console (for output)

Note, you'll need to extract all the nupkg templates into a parent directory.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using Spectre.Console;

var templates = Directory.EnumerateFiles(
    "<folder to extracted template packages>", 
    "template.json", 
    SearchOption.AllDirectories);

using var http = new HttpClient();
var jsonSchema = await http.GetStringAsync("https://json.schemastore.org/template");
var schema = JSchema.Parse(jsonSchema);

List<Problem> problems = new();
foreach (var template in templates)
{
    var data = await File.ReadAllTextAsync(template);
    var json = JToken.Parse(data);
    if (!json.IsValid(schema, out IList<string> errors))
    {
        dynamic j = json;
        // just in case name is missing
        var name = (j.name ?? template).ToString();
        problems.Add(new Problem(name, errors));
    }
}

var table = new Table()
    .AddColumn("Name")
    .AddColumn("🚨 Errors");

foreach (var problem in problems)
{
    table.AddRow(
        problem.Name, 
        problem.ErrorMessages.EscapeMarkup()
    );
}

AnsiConsole.Render(table);

public record Problem(string Name, IList<string> Errors)
{
    public string ErrorMessages =>
        Errors.Select(e => $"{e}\n")
            .Aggregate("", (a, i) => a + i);
}

Thoughts

Currently, I extract the archives (i.e. the nupkg files into a parent directory). Since these are essentially zip files, we could use the ZipFile class in .NET to just pull the template.json files out into memory then parse them, helping avoid an additional step. This would also make the tool more flexible as you could give it a directory or a specific nupkg file and it would work just the same.

License

Note that Newtonsoft.Json.Schema is AGPL or requires a license to be purchased if this tool is commercially available.

@sayedihashimi
Copy link
Owner

sayedihashimi commented May 28, 2021

@khalidabuhakmeh I don't think I can use Newtonsoft.Json.Schema, I would have to purchase a license for that. I looked at this when I started working on this tool. I also looked for alternatives and I unfortunately wasn't able to find any that I could use.
The license does have an OSS specific support, but if I'm not mistaken I would have to use the AGPL license here to leverage that.

Without using Newtonsoft.Json.Schema, I was able able to run some validation, but the errors that came out were horrible. For example, you would have one issue in your json file, and it would spit out like 10 issues.

@khalidabuhakmeh
Copy link
Author

khalidabuhakmeh commented May 28, 2021

Here is an updated version using NJsonSchema (MIT License). Still using Newtonsoft.Json for parsing the Json but that could also use System.Text.Json.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using Newtonsoft.Json.Linq;
using NJsonSchema;
using Spectre.Console;

var templates = Directory.EnumerateFiles(
    "/Users/khalidabuhakmeh/Desktop/untitled folder",
    "template.json", 
    SearchOption.AllDirectories);

using var http = new HttpClient();
var jsonSchema = await http.GetStringAsync("https://json.schemastore.org/template");
var schema = await JsonSchema.FromJsonAsync(jsonSchema);


List<Problem> problems = new();
foreach (var template in templates)
{
    var data = await File.ReadAllTextAsync(template);
    var json = JObject.Parse(data);
    
    // just in case name is missing
    var name =  json["name"]?.ToString() ?? template;
    var problem = new Problem(name, new List<string>());
    
    var results = schema.Validate(data);
    if (results.Any())
    {
        var errors = results
            .Select(e => $"{e.Kind} {e.Path} at L#{e.LineNumber} position {e.LinePosition}")
            .ToList();

        problem.Errors.AddRange(errors);
    }

    if (problem.Errors.Any())
    {
        problems.Add(problem);
    }
}

var table = new Table()
    .AddColumn("Name")
    .AddColumn("🚨 Errors");

foreach (var problem in problems)
{
    table.AddRow(
        problem.Name, 
        problem.ErrorMessages.EscapeMarkup()
    );
}

AnsiConsole.Render(table);

public record Problem(string Name, List<string> Errors)
{
    public string ErrorMessages =>
        Errors.Select(e => $"{e}\n")
            .Aggregate("", (a, i) => a + i);
}

With the output of.

┌────────────────────────┬──────────────────────────────────────────────────────────┐
│ Name                   │ 🚨 Errors                                                │
├────────────────────────┼──────────────────────────────────────────────────────────┤
│ Razor Component        │ ⁃ PropertyRequired #/shortName at L#1 position 1         │
│                        │                                                          │
│ Protocol Buffer File   │ ⁃ PropertyRequired #/shortName at L#1 position 1         │
│                        │                                                          │
│ Blazor WebAssembly App │ ⁃ ArrayItemNotValid #/postActions[1] at L#504 position 5 │
│                        │                                                          │
│ Blazor WebAssembly App │ ⁃ ArrayItemNotValid #/postActions[1] at L#504 position 5 │
│                        │                                                          │
│ Solution File          │ ⁃ PropertyRequired #/tags at L#1 position 1              │
│                        │                                                          │
│ Razor Component        │ ⁃ PropertyRequired #/shortName at L#1 position 1         │
│                        │                                                          │
│ Protocol Buffer File   │ ⁃ PropertyRequired #/shortName at L#1 position 1         │
│                        │                                                          │
│ Solution File          │ ⁃ PropertyRequired #/tags at L#1 position 1              │
│                        │                                                          │
│ Solution File          │ ⁃ PropertyRequired #/tags at L#1 position 1              │
│                        │                                                          │
│ Blazor WebAssembly App │ ⁃ ArrayItemNotValid #/postActions[1] at L#472 position 5 │
│                        │                                                          │
│ Razor Component        │ ⁃ PropertyRequired #/shortName at L#1 position 1         │
│                        │                                                          │
│ Protocol Buffer File   │ ⁃ PropertyRequired #/shortName at L#1 position 1         │
│                        │                                                          │
└────────────────────────┴──────────────────────────────────────────────────────────┘

The output has duplicates because I exported multiple target frameworks out into directories. An updated version might want to also have a column with the framework version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants