414 lines
15 KiB
C#
414 lines
15 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using UniJSON;
|
||
|
||
namespace UniGLTF.JsonSchema
|
||
{
|
||
public class JsonSchemaParser
|
||
{
|
||
DirectoryInfo[] m_dir;
|
||
Dictionary<FileInfo, byte[]> m_cache = new Dictionary<FileInfo, byte[]>();
|
||
|
||
public JsonSchemaParser(params DirectoryInfo[] dir)
|
||
{
|
||
m_dir = dir;
|
||
}
|
||
|
||
public static JsonSchemaSource Parse(string root, string jsonPath = "")
|
||
{
|
||
// setup
|
||
var path = new FileInfo(root);
|
||
var parser = new JsonSchemaParser(path.Directory);
|
||
|
||
// traverse
|
||
return parser.Load(path.Name, jsonPath);
|
||
}
|
||
|
||
public JsonSchemaSource Load(string fileName, string jsonPath)
|
||
{
|
||
JsonSchemaSource loaded = null;
|
||
foreach (var dir in m_dir)
|
||
{
|
||
var path = Path.Combine(dir.FullName, fileName);
|
||
if (File.Exists(path))
|
||
{
|
||
loaded = Load(new FileInfo(path), jsonPath);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (loaded is null)
|
||
{
|
||
throw new FileNotFoundException(fileName);
|
||
}
|
||
return loaded;
|
||
}
|
||
|
||
public JsonSchemaSource Load(FileInfo path, string jsonPath)
|
||
{
|
||
if (!m_cache.TryGetValue(path, out byte[] bytes))
|
||
{
|
||
// Console.WriteLine($"load {path}");
|
||
bytes = File.ReadAllBytes(path.FullName);
|
||
m_cache.Add(path, bytes);
|
||
}
|
||
|
||
{
|
||
var jsonSchema = Parse(bytes.ParseAsJson(), jsonPath);
|
||
jsonSchema.FilePath = path;
|
||
return jsonSchema;
|
||
}
|
||
}
|
||
|
||
void MergeTo(JsonSchemaSource src, JsonSchemaSource dst)
|
||
{
|
||
if (string.IsNullOrEmpty(dst.title))
|
||
{
|
||
dst.title = src.title;
|
||
}
|
||
if (string.IsNullOrEmpty(dst.description))
|
||
{
|
||
dst.description = src.description;
|
||
}
|
||
if (src.type != JsonSchemaType.Unknown)
|
||
{
|
||
dst.type = src.type;
|
||
}
|
||
foreach (var kv in src.EnumerateProperties())
|
||
{
|
||
dst.AddProperty(kv.Key, kv.Value);
|
||
}
|
||
if (src.enumStringValues != null)
|
||
{
|
||
dst.enumStringValues = src.enumStringValues.ToArray();
|
||
}
|
||
}
|
||
|
||
JsonSchemaSource Parse(JsonNode json, string jsonPath)
|
||
{
|
||
var source = new JsonSchemaSource
|
||
{
|
||
JsonPath = jsonPath,
|
||
};
|
||
|
||
foreach (var kv in json.ObjectItems())
|
||
{
|
||
switch (kv.Key.GetString())
|
||
{
|
||
case "$ref":
|
||
{
|
||
var reference = Load(kv.Value.GetString(), jsonPath);
|
||
MergeTo(reference, source);
|
||
break;
|
||
}
|
||
|
||
case "allOf":
|
||
// glTF では継承として使われる
|
||
{
|
||
var reference = AllOf(kv.Value, jsonPath);
|
||
MergeTo(reference, source);
|
||
break;
|
||
}
|
||
|
||
case "$schema":
|
||
break;
|
||
|
||
case "type":
|
||
source.type = (JsonSchemaType)Enum.Parse(typeof(JsonSchemaType), kv.Value.GetString(), true);
|
||
break;
|
||
|
||
case "title":
|
||
source.title = kv.Value.GetString();
|
||
break;
|
||
|
||
case "description":
|
||
source.description = kv.Value.GetString();
|
||
break;
|
||
|
||
case "gltf_detailedDescription":
|
||
source.gltfDetail = kv.Value.GetString();
|
||
break;
|
||
|
||
case "default":
|
||
break;
|
||
|
||
case "gltf_webgl":
|
||
break;
|
||
|
||
case "anyOf":
|
||
// glTF ではenumとして使われる
|
||
ParseAnyOfAsEnum(ref source, kv.Value);
|
||
break;
|
||
|
||
case "oneOf":
|
||
// TODO: union 的な
|
||
break;
|
||
|
||
case "not":
|
||
// TODO: プロパティの両立を禁止する、排他的な
|
||
break;
|
||
|
||
case "pattern":
|
||
source.pattern = kv.Value.GetString();
|
||
break;
|
||
|
||
case "format":
|
||
// TODO
|
||
break;
|
||
|
||
case "gltf_uriType":
|
||
// TODO
|
||
break;
|
||
|
||
case "minimum":
|
||
if (source.type != JsonSchemaType.Number && source.type != JsonSchemaType.Integer) throw new Exception();
|
||
source.minimum = kv.Value.GetDouble();
|
||
break;
|
||
|
||
case "exclusiveMinimum":
|
||
if (source.type != JsonSchemaType.Number && source.type != JsonSchemaType.Integer) throw new Exception();
|
||
source.exclusiveMinimum = kv.Value.GetBoolean(); // ?
|
||
break;
|
||
|
||
case "maximum":
|
||
if (source.type != JsonSchemaType.Number && source.type != JsonSchemaType.Integer) throw new Exception();
|
||
source.maximum = kv.Value.GetDouble();
|
||
break;
|
||
|
||
case "multipleOf":
|
||
if (source.type != JsonSchemaType.Number && source.type != JsonSchemaType.Integer) throw new Exception();
|
||
source.multipleOf = kv.Value.GetDouble();
|
||
break;
|
||
|
||
case "properties":
|
||
if (source.type != JsonSchemaType.Object) throw new Exception();
|
||
// source.properties = new Dictionary<string, JsonSchemaSource>();
|
||
foreach (var prop in kv.Value.ObjectItems())
|
||
{
|
||
var key = prop.Key.GetString();
|
||
var propJsonPath = $"{jsonPath}.{key}";
|
||
var propSchema = Parse(prop.Value, propJsonPath);
|
||
if (string.IsNullOrEmpty(propSchema.title))
|
||
{
|
||
propSchema.title = key.ToUpperCamel();
|
||
}
|
||
if (propSchema is null)
|
||
{
|
||
if (source.baseSchema is null)
|
||
{
|
||
// add empty object. extras
|
||
source.AddProperty(prop.Key.GetString(), new JsonSchemaSource
|
||
{
|
||
JsonPath = propJsonPath,
|
||
type = JsonSchemaType.Object,
|
||
});
|
||
}
|
||
// else if (source.baseSchema.GetPropertyFromPath(propJsonPath))
|
||
// {
|
||
// // ok
|
||
// }
|
||
else
|
||
{
|
||
throw new Exception("unknown");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (source.GetProperty(prop.Key.GetString()) == null)
|
||
{
|
||
source.AddProperty(prop.Key.GetString(), propSchema);
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
|
||
case "required":
|
||
source.required = kv.Value.ArrayItems().Select(x => x.GetString()).ToArray();
|
||
break;
|
||
|
||
case "dependencies":
|
||
// Property間の依存関係?
|
||
// TODO:
|
||
break;
|
||
|
||
case "additionalProperties":
|
||
if (source.type != JsonSchemaType.Object) throw new Exception();
|
||
if (kv.Value.Value.ValueType == ValueNodeType.Object)
|
||
{
|
||
source.additionalProperties = Parse(kv.Value, $"{jsonPath}{{}}");
|
||
}
|
||
else if (kv.Value.Value.ValueType == ValueNodeType.Boolean && kv.Value.GetBoolean() == false)
|
||
{
|
||
// skip. do nothing
|
||
}
|
||
else
|
||
{
|
||
throw new NotImplementedException();
|
||
}
|
||
break;
|
||
|
||
case "minProperties":
|
||
if (source.type != JsonSchemaType.Object) throw new Exception();
|
||
source.minProperties = kv.Value.GetInt32();
|
||
break;
|
||
|
||
case "items":
|
||
if (source.type != JsonSchemaType.Array) throw new Exception();
|
||
source.items = Parse(kv.Value, $"{jsonPath}[]");
|
||
break;
|
||
|
||
case "uniqueItems":
|
||
if (source.type != JsonSchemaType.Array) throw new Exception();
|
||
source.uniqueItems = kv.Value.GetBoolean();
|
||
break;
|
||
|
||
case "maxItems":
|
||
if (source.type != JsonSchemaType.Array) throw new Exception();
|
||
source.maxItems = kv.Value.GetInt32();
|
||
break;
|
||
|
||
case "minItems":
|
||
if (source.type != JsonSchemaType.Array) throw new Exception();
|
||
source.minItems = kv.Value.GetInt32();
|
||
break;
|
||
|
||
case "enum":
|
||
if (source.type == JsonSchemaType.String)
|
||
{
|
||
ParseStringEnum(ref source, kv.Value);
|
||
}
|
||
else if (source.type == JsonSchemaType.Integer
|
||
|| source.type == JsonSchemaType.Number)
|
||
{
|
||
throw new NotImplementedException();
|
||
}
|
||
else
|
||
{
|
||
throw new NotImplementedException();
|
||
}
|
||
break;
|
||
|
||
default:
|
||
Console.WriteLine($"unknown property: {kv.Key.GetString()} => {kv.Value}");
|
||
break;
|
||
}
|
||
}
|
||
|
||
return source;
|
||
}
|
||
|
||
void ParseStringEnum(ref JsonSchemaSource source, JsonNode json)
|
||
{
|
||
source.enumStringValues = json.ArrayItems().Select(x => x.GetString()).ToArray();
|
||
source.type = JsonSchemaType.EnumString;
|
||
}
|
||
|
||
void ParseAnyOfAsEnum(ref JsonSchemaSource source, JsonNode json)
|
||
{
|
||
List<int> values = new List<int>();
|
||
List<string> stringValues = new List<string>();
|
||
List<string> descriptions = new List<string>();
|
||
foreach (var v in json.ArrayItems())
|
||
{
|
||
foreach (var kv in v.ObjectItems())
|
||
{
|
||
switch (kv.Key.GetString())
|
||
{
|
||
case "enum":
|
||
{
|
||
int i = 0;
|
||
foreach (var a in kv.Value.ArrayItems())
|
||
{
|
||
switch (a.Value.ValueType)
|
||
{
|
||
case ValueNodeType.Number:
|
||
case ValueNodeType.Integer:
|
||
values.Add(a.GetInt32());
|
||
break;
|
||
|
||
case ValueNodeType.String:
|
||
stringValues.Add(a.GetString());
|
||
break;
|
||
|
||
default:
|
||
throw new NotImplementedException();
|
||
}
|
||
++i;
|
||
}
|
||
}
|
||
break;
|
||
|
||
case "description":
|
||
{
|
||
descriptions.Add(kv.Value.GetString());
|
||
}
|
||
break;
|
||
|
||
case "type":
|
||
break;
|
||
|
||
default:
|
||
throw new NotImplementedException();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (stringValues.Count > 0)
|
||
{
|
||
if (values.Count == 0)
|
||
{
|
||
source.enumStringValues = stringValues.ToArray();
|
||
source.type = JsonSchemaType.EnumString;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (descriptions.Count == values.Count)
|
||
{
|
||
source.enumValues = new KeyValuePair<string, int>[values.Count];
|
||
for (int i = 0; i < values.Count; ++i)
|
||
{
|
||
source.enumValues[i] = new KeyValuePair<string, int>
|
||
(
|
||
descriptions[i],
|
||
values[i]
|
||
);
|
||
}
|
||
source.type = JsonSchemaType.Enum;
|
||
return;
|
||
}
|
||
|
||
throw new NotImplementedException();
|
||
}
|
||
|
||
JsonSchemaSource AllOf(JsonNode json, string jsonPath)
|
||
{
|
||
string refValue = null;
|
||
int count = 0;
|
||
foreach (var a in json.ArrayItems())
|
||
{
|
||
foreach (var kv in a.ObjectItems())
|
||
{
|
||
if (kv.Key.GetString() != "$ref")
|
||
{
|
||
throw new NotImplementedException();
|
||
}
|
||
|
||
refValue = kv.Value.GetString();
|
||
|
||
++count;
|
||
}
|
||
}
|
||
if (count != 1)
|
||
{
|
||
throw new NotImplementedException();
|
||
}
|
||
|
||
var reference = Load(refValue, jsonPath);
|
||
return reference;
|
||
}
|
||
}
|
||
}
|