Express discriminated union in JSON schema (#7)

This commit is contained in:
Patrick Stevens
2022-12-30 12:27:54 +00:00
committed by GitHub
parent f0f8504234
commit 99df76b22b
8 changed files with 209 additions and 135 deletions

View File

@@ -111,8 +111,21 @@ type GiteaConfig =
static member internal OfSerialised (s : SerialisedGiteaConfig) =
{
GiteaConfig.Users = s.Users |> Map.map (fun _ -> UserInfo.OfSerialised)
Repos = s.Repos |> Map.map (fun _ -> Map.map (fun _ -> Repo.OfSerialised))
GiteaConfig.Users =
s.Users
|> Seq.map (fun (KeyValue (user, info)) -> user, UserInfo.OfSerialised info)
|> Map.ofSeq
Repos =
s.Repos
|> Seq.map (fun (KeyValue (user, repos)) ->
let repos =
repos
|> Seq.map (fun (KeyValue (repoName, repo)) -> repoName, Repo.OfSerialised repo)
|> Map.ofSeq
user, repos
)
|> Map.ofSeq
}
[<RequireQualifiedAccess>]

View File

@@ -178,7 +178,6 @@ module Gitea =
with e ->
raise (AggregateException ($"Error migrating {user}:{r}", e))
| None, None ->
// TODO: express this in JsonSchema
failwith $"You must supply exactly one of Native or GitHub for {user}:{r}."
| Some _, Some _ ->
failwith $"Repo {user}:{r} has both Native and GitHub set; you must set exactly one."

View File

@@ -1,87 +1,12 @@
{
"definitions": {
"Nullable<SerialisedNativeRepo>": {
"description": "If this repo is to be created natively on Gitea, the information about the repo.",
"type": [
"object",
"null"
],
"additionalProperties": false,
"properties": {
"defaultBranch": {
"description": "The default branch name for this repository, e.g. 'main'",
"type": "string"
},
"private": {
"description": "Whether this repository is a Gitea private repo",
"type": "boolean"
}
},
"required": [
"defaultBranch"
]
},
"SerialisedRepo": {
"type": [
"object",
"null"
],
"additionalProperties": false,
"properties": {
"description": {
"description": "The text that will accompany this repository in the Gitea UI",
"type": "string"
},
"gitHub": {
"description": "If this repo is to sync from GitHub, the URI (e.g. 'https://github.com/Smaug123/nix-maui')",
"type": [
"string",
"null"
],
"format": "uri"
},
"native": {
"$ref": "#/definitions/Nullable<SerialisedNativeRepo>"
}
},
"required": [
"description"
]
},
"SerialisedUserInfo": {
"type": [
"object",
"null"
],
"additionalProperties": false,
"properties": {
"isAdmin": {
"type": "boolean"
},
"email": {
"type": "string"
},
"website": {
"type": [
"string",
"null"
],
"format": "uri"
},
"visibility": {
"type": [
"string",
"null"
]
}
},
"required": [
"email"
]
}
},
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "SerialisedGiteaConfig",
"type": "object",
"additionalProperties": false,
"required": [
"users",
"repos"
],
"properties": {
"users": {
"type": "object",
@@ -92,18 +17,102 @@
"repos": {
"type": "object",
"additionalProperties": {
"type": [
"object",
"null"
],
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/SerialisedRepo"
}
}
}
},
"required": [
"users",
"repos"
]
"definitions": {
"SerialisedUserInfo": {
"type": "object",
"additionalProperties": false,
"required": [
"email"
],
"properties": {
"isAdmin": {
"type": "boolean"
},
"email": {
"type": "string"
},
"website": {
"type": [
"null",
"string"
],
"format": "uri"
},
"visibility": {
"type": [
"null",
"string"
]
}
}
},
"SerialisedRepo": {
"type": "object",
"additionalProperties": false,
"properties": {
"description": {
"type": "string",
"description": "The text that will accompany this repository in the Gitea UI"
},
"gitHub": {
"type": [
"null",
"string"
],
"description": "If this repo is to sync from GitHub, the URI (e.g. 'https://github.com/Smaug123/nix-maui')",
"format": "uri"
},
"native": {
"description": "If this repo is to be created natively on Gitea, the information about the repo.",
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/SerialisedNativeRepo"
}
]
}
},
"oneOf": [
{
"required": [
"description",
"gitHub"
]
},
{
"required": [
"description",
"native"
]
}
]
},
"SerialisedNativeRepo": {
"type": "object",
"description": "Information about a repo that is to be created on Gitea without syncing from GitHub.",
"additionalProperties": false,
"required": [
"defaultBranch"
],
"properties": {
"defaultBranch": {
"type": "string",
"description": "The default branch name for this repository, e.g. 'main'"
},
"private": {
"type": "boolean",
"description": "Whether this repository is a Gitea private repo"
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
namespace Gitea.Declarative
open System
open System.Collections.Generic
open System.ComponentModel
open Newtonsoft.Json
@@ -51,7 +52,7 @@ type internal SerialisedUserInfo =
type internal SerialisedGiteaConfig =
{
[<JsonProperty(Required = Required.Always)>]
Users : Map<User, SerialisedUserInfo>
Users : Dictionary<User, SerialisedUserInfo>
[<JsonProperty(Required = Required.Always)>]
Repos : Map<User, Map<RepoName, SerialisedRepo>>
Repos : Dictionary<User, Dictionary<RepoName, SerialisedRepo>>
}

View File

@@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="FsUnit" Version="5.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="NJsonSchema" Version="10.8.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />

View File

@@ -3,53 +3,39 @@ namespace Gitea.Declarative.Test
open System.IO
open System.Reflection
open Gitea.Declarative
open NJsonSchema.Generation
open NJsonSchema.Validation
open NUnit.Framework
open FsUnitTyped
open NJsonSchema
open Newtonsoft.Json
open Newtonsoft.Json.Schema
open Newtonsoft.Json.Schema.Generation
open Newtonsoft.Json.Serialization
[<TestFixture>]
module TestSchema =
let schemaGen = JSchemaGenerator ()
schemaGen.ContractResolver <- CamelCasePropertyNamesContractResolver ()
[<Test>]
let ``Schema is consistent`` () =
let schemaFile =
Assembly.GetExecutingAssembly().Location
|> FileInfo
|> fun fi -> fi.Directory
|> Utils.findFileAbove "Gitea.Declarative.Lib/GiteaConfig.schema.json"
let existing = JSchema.Parse (File.ReadAllText schemaFile.FullName)
let derived = schemaGen.Generate typeof<SerialisedGiteaConfig>
existing.ToString () |> shouldEqual (derived.ToString ())
[<Test>]
let ``Example conforms to schema`` () =
let executing = Assembly.GetExecutingAssembly().Location |> FileInfo
let schemaFile = Utils.findFileAbove "GiteaConfig.json" executing.Directory
let existing = JSchema.Parse (File.ReadAllText schemaFile.FullName)
let schemaFile =
Utils.findFileAbove "Gitea.Declarative.Lib/GiteaConfig.schema.json" executing.Directory
let schema = JsonSchema.FromJsonAsync(File.ReadAllText schemaFile.FullName).Result
let jsonFile = Utils.findFileAbove "GiteaConfig.json" executing.Directory
let json = File.ReadAllText jsonFile.FullName
use reader = new JsonTextReader (new StringReader (json))
use validatingReader = new JSchemaValidatingReader (reader)
validatingReader.Schema <- existing
let validator = JsonSchemaValidator ()
let errors = validator.Validate (json, schema)
let messages = ResizeArray ()
validatingReader.ValidationEventHandler.Add (fun args -> messages.Add args.Message)
errors |> shouldBeEmpty
let ser = JsonSerializer ()
ser.ContractResolver <- CamelCasePropertyNamesContractResolver ()
let _config = ser.Deserialize<SerialisedGiteaConfig> validatingReader
messages |> shouldBeEmpty
[<Test>]
let ``Example can be loaded`` () =
let executing = Assembly.GetExecutingAssembly().Location |> FileInfo
let jsonFile = Utils.findFileAbove "GiteaConfig.json" executing.Directory
GiteaConfig.get jsonFile |> ignore
[<Test>]
[<Explicit "Run this to regenerate the schema file">]
@@ -60,6 +46,27 @@ module TestSchema =
|> fun fi -> fi.Directory
|> Utils.findFileAbove "Gitea.Declarative.Lib/GiteaConfig.schema.json"
let schema = schemaGen.Generate typeof<SerialisedGiteaConfig>
let settings = JsonSchemaGeneratorSettings ()
File.WriteAllText (schemaFile.FullName, schema.ToString ())
settings.SerializerSettings <-
JsonSerializerSettings (ContractResolver = CamelCasePropertyNamesContractResolver ())
let schema = JsonSchema.FromType (typeof<SerialisedGiteaConfig>, settings)
// Hack around the lack of discriminated unions in C#
let serialisedRepoSchema = schema.Definitions.[typeof<SerialisedRepo>.Name]
serialisedRepoSchema.RequiredProperties.Clear ()
do
let schema = JsonSchema ()
schema.RequiredProperties.Add "description"
schema.RequiredProperties.Add "gitHub"
serialisedRepoSchema.OneOf.Add schema
do
let schema = JsonSchema ()
schema.RequiredProperties.Add "description"
schema.RequiredProperties.Add "native"
serialisedRepoSchema.OneOf.Add schema
File.WriteAllText (schemaFile.FullName, schema.ToJson ())

View File

@@ -5,7 +5,7 @@ This is a small project to allow you to specify a [Gitea](https://github.com/go-
# How to build and run
With Nix: `nix run github:Smaug123/dotnet-gitea-declarative -- --help`.
The config file you provide as an argument should conform to [the schema](./Gitea.Declarative.Lib/GiteaConfig.schema.json).
The config file you provide as an argument should conform to [the schema](./Gitea.Declarative.Lib/GiteaConfig.schema.json); there is [an example](./Gitea.Declarative.Test/GiteaConfig.json) in the tests.
## Building from source

View File

@@ -51,6 +51,11 @@
version = "4.0.1";
sha256 = "0zxc0apx1gcx361jlq8smc9pfdgmyjh6hpka8dypc9w23nlsh6yj";
})
(fetchNuGet {
pname = "Microsoft.CSharp";
version = "4.3.0";
sha256 = "0gw297dgkh0al1zxvgvncqs0j15lsna9l1wpqas4rflmys440xvb";
})
(fetchNuGet {
pname = "Microsoft.Extensions.Configuration";
version = "7.0.0";
@@ -166,6 +171,11 @@
version = "4.3.0";
sha256 = "0j0c1wj4ndj21zsgivsc24whiya605603kxrbiw6wkfdync464wq";
})
(fetchNuGet {
pname = "Namotion.Reflection";
version = "2.1.0";
sha256 = "0ql10m9i5qm3cmcw6abk6wvm823vc4s8wzx351yffd6syd50mkb7";
})
(fetchNuGet {
pname = "Nerdbank.GitVersioning";
version = "3.5.119";
@@ -181,11 +191,6 @@
version = "2.1.0";
sha256 = "12n76gymxq715lkrw841vi5r84kx746cxxssp22pd08as75jzsj6";
})
(fetchNuGet {
pname = "Newtonsoft.Json";
version = "12.0.3";
sha256 = "17dzl305d835mzign8r15vkmav2hq8l6g7942dfjpnzr17wwl89x";
})
(fetchNuGet {
pname = "Newtonsoft.Json";
version = "13.0.2";
@@ -197,9 +202,9 @@
sha256 = "0mcy0i7pnfpqm4pcaiyzzji4g0c8i3a5gjz28rrr28110np8304r";
})
(fetchNuGet {
pname = "Newtonsoft.Json.Schema";
version = "3.0.14";
sha256 = "1njk1arrf8pbx0i0p3yww459i70p0fcx02vs0jnbb6znvcy4mvh6";
pname = "NJsonSchema";
version = "10.8.0";
sha256 = "1mzqskv4vx5mzq0rykjwgc323afs2km0hslr2xr6r9fz9qygd28h";
})
(fetchNuGet {
pname = "NuGet.Frameworks";
@@ -556,6 +561,11 @@
version = "4.0.11";
sha256 = "1pla2dx8gkidf7xkciig6nifdsb494axjvzvann8g2lp3dbqasm9";
})
(fetchNuGet {
pname = "System.Dynamic.Runtime";
version = "4.3.0";
sha256 = "1d951hrvrpndk7insiag80qxjbf2y0y39y8h5hnq9612ws661glk";
})
(fetchNuGet {
pname = "System.Globalization";
version = "4.0.11";
@@ -621,6 +631,11 @@
version = "4.1.0";
sha256 = "1gpdxl6ip06cnab7n3zlcg6mqp7kknf73s8wjinzi4p0apw82fpg";
})
(fetchNuGet {
pname = "System.Linq.Expressions";
version = "4.3.0";
sha256 = "0ky2nrcvh70rqq88m9a5yqabsl4fyd17bpr63iy2mbivjs2nyypv";
})
(fetchNuGet {
pname = "System.Memory";
version = "4.5.4";
@@ -656,6 +671,11 @@
version = "4.0.12";
sha256 = "1sybkfi60a4588xn34nd9a58png36i0xr4y4v4kqpg8wlvy5krrj";
})
(fetchNuGet {
pname = "System.ObjectModel";
version = "4.3.0";
sha256 = "191p63zy5rpqx7dnrb3h7prvgixmk168fhvvkkvhlazncf8r3nc2";
})
(fetchNuGet {
pname = "System.Private.Uri";
version = "4.3.0";
@@ -676,21 +696,41 @@
version = "4.0.1";
sha256 = "0ydqcsvh6smi41gyaakglnv252625hf29f7kywy2c70nhii2ylqp";
})
(fetchNuGet {
pname = "System.Reflection.Emit";
version = "4.3.0";
sha256 = "11f8y3qfysfcrscjpjym9msk7lsfxkk4fmz9qq95kn3jd0769f74";
})
(fetchNuGet {
pname = "System.Reflection.Emit.ILGeneration";
version = "4.0.1";
sha256 = "1pcd2ig6bg144y10w7yxgc9d22r7c7ww7qn1frdfwgxr24j9wvv0";
})
(fetchNuGet {
pname = "System.Reflection.Emit.ILGeneration";
version = "4.3.0";
sha256 = "0w1n67glpv8241vnpz1kl14sy7zlnw414aqwj4hcx5nd86f6994q";
})
(fetchNuGet {
pname = "System.Reflection.Emit.Lightweight";
version = "4.0.1";
sha256 = "1s4b043zdbx9k39lfhvsk68msv1nxbidhkq6nbm27q7sf8xcsnxr";
})
(fetchNuGet {
pname = "System.Reflection.Emit.Lightweight";
version = "4.3.0";
sha256 = "0ql7lcakycrvzgi9kxz1b3lljd990az1x6c4jsiwcacrvimpib5c";
})
(fetchNuGet {
pname = "System.Reflection.Extensions";
version = "4.0.1";
sha256 = "0m7wqwq0zqq9gbpiqvgk3sr92cbrw7cp3xn53xvw7zj6rz6fdirn";
})
(fetchNuGet {
pname = "System.Reflection.Extensions";
version = "4.3.0";
sha256 = "02bly8bdc98gs22lqsfx9xicblszr2yan7v2mmw3g7hy6miq5hwq";
})
(fetchNuGet {
pname = "System.Reflection.Metadata";
version = "1.6.0";
@@ -711,6 +751,11 @@
version = "4.1.0";
sha256 = "1bjli8a7sc7jlxqgcagl9nh8axzfl11f4ld3rjqsyxc516iijij7";
})
(fetchNuGet {
pname = "System.Reflection.TypeExtensions";
version = "4.3.0";
sha256 = "0y2ssg08d817p0vdag98vn238gyrrynjdj4181hdg780sif3ykp1";
})
(fetchNuGet {
pname = "System.Resources.ResourceManager";
version = "4.0.1";