diff --git a/JsonLD.Infrastructure.Text.Tests/ConformanceTests.cs b/JsonLD.Infrastructure.Text.Tests/ConformanceTests.cs new file mode 100644 index 0000000..6b86cb5 --- /dev/null +++ b/JsonLD.Infrastructure.Text.Tests/ConformanceTests.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Xunit; +using System.IO; +using JsonLD.Core; +using JsonLD.Util; +using JsonLD.Test; + +namespace JsonLD.Infrastructure.Text.Tests +{ + public class ConformanceTests + { + [Theory, ClassData(typeof(ConformanceCases))] + public void ConformanceTestPasses(string id, ConformanceCase conformanceCase) + { + string result; + try + { + result = conformanceCase.run(); + if (!JsonLdUtils.DeepCompareStrings(result, conformanceCase.output)) + { +#if DEBUG + Console.WriteLine(id); + Console.WriteLine("Actual:"); + Console.Write(JSONUtils.ToPrettyString(result)); + Console.WriteLine("--------------------------"); + Console.WriteLine("Expected:"); + Console.Write(JSONUtils.ToPrettyString(conformanceCase.output)); + Console.WriteLine("--------------------------"); +#endif + + Assert.True(false, "Returned JSON doesn't match expectations."); + } + + } + catch (Exception ex) + { + if (conformanceCase.error == default) throw; // unexpected error + Assert.True(ex.Message.StartsWith(conformanceCase.error), "Resulting error doesn't match expectations."); + } + } + } + + public class ConformanceCase + { + public string input { get; set; } + public string context { get; set; } + public string frame { get; set; } + public string output { get; set; } + public string error { get; set; } + public Func run { get; set; } + } + + public class ConformanceCases : IEnumerable + { + string[] manifests = new[] { + "compact-manifest.jsonld", + "expand-manifest.jsonld", + "flatten-manifest.jsonld", + "frame-manifest.jsonld", + "toRdf-manifest.jsonld", + "fromRdf-manifest.jsonld", + "normalize-manifest.jsonld", +// Test tests are not supported on CORE CLR +#if !PORTABLE && !IS_CORECLR + "error-manifest.jsonld", + "remote-doc-manifest.jsonld", +#endif + }; + + public ConformanceCases() + { + + } + + public IEnumerator GetEnumerator() + { + var rootDirectory = "W3C"; + + foreach (string manifest in manifests) + { + var testCases = JsonFetcher.GetTestCases(manifest, rootDirectory); + + foreach (var testCase in testCases.Sequence) + { + Func run; + ConformanceCase newCase = new ConformanceCase(); + + try + { + newCase.input = testCase.GetInputJson(); + newCase.context = testCase.GetContextJson(); + newCase.frame = testCase.GetFrameJson(); + } + catch (Exception ex) + { + throw new Exception($"{testCase.Id} in {testCases.Name}", ex); + } + + var options = new JsonLD.Infrastructure.Text.JsonLdOptions("http://json-ld.org/test-suite/tests/" + testCase.Input); + + var testType = testCase.Type; + + if (testType.Any((s) => s == "jld:NegativeEvaluationTest")) + { + newCase.error = testCase.Expect; + } + else if (testType.Any((s) => s == "jld:PositiveEvaluationTest")) + { + if (testType.Any((s) => new List { "jld:ToRDFTest", "jld:NormalizeTest" }.Contains(s))) + { + newCase.output = File.ReadAllText(Path.Combine("W3C", testCase.Expect)); + } + else if (testType.Any((s) => s == "jld:FromRDFTest")) + { + newCase.input = File.ReadAllText(Path.Combine("W3C", testCase.Input)); + newCase.output = testCase.GetExpectJson(); + } + else + { + newCase.output = testCase.GetExpectJson(); + } + } + else + { + throw new Exception("Expecting either positive or negative evaluation test."); + } + + if (testCase.Options != null) + { + if (testCase.Options.CompactArrays.HasValue) + { + options.SetCompactArrays(testCase.Options.CompactArrays.Value); + } + if (testCase.Options.Base != default) + { + options.SetBase(testCase.Options.Base); + } + if (testCase.Options.ExpandContext != default) + { + newCase.context = testCase.GetExpandContextJson(); + options.SetExpandContext(newCase.context); + } + if (testCase.Options.ProduceGeneralizedRdf.HasValue) + { + options.SetProduceGeneralizedRdf(testCase.Options.ProduceGeneralizedRdf.Value); + } + if (testCase.Options.UseNativeTypes.HasValue) + { + options.SetUseNativeTypes(testCase.Options.UseNativeTypes.Value); + } + if (testCase.Options.UseRdfType.HasValue) + { + options.SetUseRdfType(testCase.Options.UseRdfType.Value); + } + } + + if (testType.Any((s) => s == "jld:CompactTest")) + { + run = () => Infrastructure.Text.JsonLdProcessor.Compact(newCase.input, newCase.context, options); + } + else if (testType.Any((s) => s == "jld:ExpandTest")) + { + run = () => Infrastructure.Text.JsonLdProcessor.Expand(newCase.input, options); + } + else if (testType.Any((s) => s == "jld:FlattenTest")) + { + run = () => Infrastructure.Text.JsonLdProcessor.Flatten(newCase.input, newCase.context, options); + } + else if (testType.Any((s) => s == "jld:FrameTest")) + { + run = () => Infrastructure.Text.JsonLdProcessor.Frame(newCase.input, newCase.frame, options); + } + else if (testType.Any((s) => s == "jld:NormalizeTest")) + { + run = () => RDFDatasetUtils.ToNQuads((RDFDataset)Infrastructure.Text.JsonLdProcessor.Normalize(newCase.input, options)).Replace("\n", "\r\n"); + } + else if (testType.Any((s) => s == "jld:ToRDFTest")) + { + options.format = "application/nquads"; + run = () => Infrastructure.Text.JsonLdProcessor.ToRDF(newCase.input, options).Replace("\n", "\r\n"); + + } + else if (testType.Any((s) => s == "jld:FromRDFTest")) + { + options.format = "application/nquads"; + run = () => Infrastructure.Text.JsonLdProcessor.FromRDF(newCase.input, options); + } + else + { + run = () => { throw new Exception("Couldn't find a test type, apparently."); }; + } + + if (testCases.AreRemoteDocumentTests) + { + Func innerRun = run; + run = () => + { + var remoteDoc = options.documentLoader.LoadDocument("https://json-ld.org/test-suite/tests/" + testCase.Input); + newCase.input = remoteDoc.Document; + options.SetBase(remoteDoc.DocumentUrl); + options.SetExpandContext(remoteDoc.Context); + return innerRun(); + }; + } + + newCase.run = run; + + yield return new object[] { manifest + testCase.Id, newCase }; + } + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + throw new Exception("auggh"); + } + } +} diff --git a/JsonLD.Infrastructure.Text.Tests/DictionaryExtensions.cs b/JsonLD.Infrastructure.Text.Tests/DictionaryExtensions.cs new file mode 100644 index 0000000..557b788 --- /dev/null +++ b/JsonLD.Infrastructure.Text.Tests/DictionaryExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace JsonLD.Infrastructure.Text.Tests +{ + internal static class DictionaryExtensions + { + public static T Required(this Dictionary dictionary, string propertyName) => Extract(dictionary, propertyName, true); + + public static T Optional(this Dictionary dictionary, string propertyName) => Extract(dictionary, propertyName, false); + + private static T Extract(Dictionary dictionary, string propertyName, bool isRequired) + { + if (!dictionary.ContainsKey(propertyName)) + { + if (isRequired) + { + var message = $"Expected top-level property {propertyName} but only found {string.Join(", ", dictionary.Keys)}"; + throw new Exception(message); + } + else + { + return default; + } + } + var value = dictionary[propertyName]; + if (typeof(T).IsGenericType) { + var genericType = typeof(T).GetGenericTypeDefinition(); + if (genericType == typeof(List<>)) + { + var list = Activator.CreateInstance(); + var addMethod = typeof(T).GetMethod("Add"); + foreach(var item in (IEnumerable)value) + { + addMethod.Invoke(list, new[] { item }); + } + return list; + } + } + return (T)value; + } + } +} \ No newline at end of file diff --git a/JsonLD.Infrastructure.Text.Tests/JsonFetcher.cs b/JsonLD.Infrastructure.Text.Tests/JsonFetcher.cs new file mode 100644 index 0000000..8a3e175 --- /dev/null +++ b/JsonLD.Infrastructure.Text.Tests/JsonFetcher.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace JsonLD.Infrastructure.Text.Tests +{ + internal static class JsonFetcher + { + public static JsonTestCases GetTestCases(string manifest, string rootDirectory) + { + var json = GetJsonAsString(rootDirectory, manifest); + var parsed = TinyJson.JSONParser.FromJson>(json); + var sequence = parsed.Required>("sequence"); + var name = parsed.Required("name"); + return new JsonTestCases(name, sequence, rootDirectory); + } + + internal static string GetJsonAsString(string folderPath, string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentOutOfRangeException(nameof(fileName), "Empty or whitespace"); + var filePath = Path.Combine(folderPath, fileName); + using (var manifestStream = File.OpenRead(filePath)) + using (var reader = new StreamReader(manifestStream)) + { + return reader.ReadToEnd(); + } + } + + internal static string GetRemoteJsonAsString(string input) => throw new NotImplementedException("Not even sure if this should be implemented. Need to double check whether remote test cases are supposed to use the core library to resolve the remote document or whether it's valid for the test case itself to retrieve it"); + + } +} \ No newline at end of file diff --git a/JsonLD.Infrastructure.Text.Tests/JsonLD.Infrastructure.Text.Tests.csproj b/JsonLD.Infrastructure.Text.Tests/JsonLD.Infrastructure.Text.Tests.csproj new file mode 100644 index 0000000..5658c23 --- /dev/null +++ b/JsonLD.Infrastructure.Text.Tests/JsonLD.Infrastructure.Text.Tests.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + diff --git a/JsonLD.Infrastructure.Text.Tests/JsonTestCase.cs b/JsonLD.Infrastructure.Text.Tests/JsonTestCase.cs new file mode 100644 index 0000000..034a77e --- /dev/null +++ b/JsonLD.Infrastructure.Text.Tests/JsonTestCase.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace JsonLD.Infrastructure.Text.Tests +{ + internal class JsonTestCase + { + private readonly string _dataPath; + private readonly bool _isRemoteDocumentTest; + + internal JsonTestCase(Dictionary dictionary, string dataPath, bool isRemoteDocumentTest) + { + Id = dictionary.Required("@id"); + Type = dictionary.Optional>("@type"); + Input = dictionary.Required("input"); + Expect = dictionary.Required("expect"); + Context = dictionary.Optional("context"); + Frame = dictionary.Optional("frame"); + var options = dictionary.Optional>("option"); + if (options != null) Options = new JsonTestCaseOptions(options); + _dataPath = dataPath; + _isRemoteDocumentTest = isRemoteDocumentTest; + } + + internal string Context { get; } + + internal string Expect { get; } + + internal string Frame { get; } + + internal string Id { get; } + + internal string Input { get; } + + internal JsonTestCaseOptions Options { get; } + + internal IEnumerable Type { get; } + + internal string GetContextJson() => Context == null ? null : JsonFetcher.GetJsonAsString(_dataPath, Context); + + internal string GetExpandContextJson() => Options?.ExpandContext == null ? null : JsonFetcher.GetJsonAsString(_dataPath, Options.ExpandContext); + + internal string GetExpectJson() => Expect == null ? null : JsonFetcher.GetJsonAsString(_dataPath, Expect); + + internal string GetFrameJson() => Frame == null ? null : JsonFetcher.GetJsonAsString(_dataPath, Frame); + + internal string GetInputJson() => _isRemoteDocumentTest + ? JsonFetcher.GetRemoteJsonAsString(Input) + : JsonFetcher.GetJsonAsString(_dataPath, Input); + } +} \ No newline at end of file diff --git a/JsonLD.Infrastructure.Text.Tests/JsonTestCaseOptions.cs b/JsonLD.Infrastructure.Text.Tests/JsonTestCaseOptions.cs new file mode 100644 index 0000000..bcfdd76 --- /dev/null +++ b/JsonLD.Infrastructure.Text.Tests/JsonTestCaseOptions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace JsonLD.Infrastructure.Text.Tests +{ + internal class JsonTestCaseOptions + { + internal JsonTestCaseOptions(Dictionary options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + CompactArrays = options.Optional("compactArrays"); + Base = options.Optional("base"); + ExpandContext = options.Optional("expandContext"); // this is temporary and incorrect + ProduceGeneralizedRdf = options.Optional("produceGeneralizedRdf"); + UseNativeTypes = options.Optional("useNativeTypes"); + UseRdfType = options.Optional("useRdfType"); + } + + internal string Base { get; private set; } + + internal bool? CompactArrays { get; private set; } + + internal string ExpandContext { get; private set; } + + internal bool? ProduceGeneralizedRdf { get; private set; } + + internal bool? UseNativeTypes { get; private set; } + + internal bool? UseRdfType { get; private set; } + } +} \ No newline at end of file diff --git a/JsonLD.Infrastructure.Text.Tests/JsonTestCases.cs b/JsonLD.Infrastructure.Text.Tests/JsonTestCases.cs new file mode 100644 index 0000000..a8d5867 --- /dev/null +++ b/JsonLD.Infrastructure.Text.Tests/JsonTestCases.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; + +namespace JsonLD.Infrastructure.Text.Tests +{ + internal class JsonTestCases + { + public JsonTestCases(string name, List sequence, string dataPath) + { + Name = name; + Sequence = sequence.Cast>().Select(dictionary => new JsonTestCase(dictionary, dataPath, AreRemoteDocumentTests)); + } + + public bool AreRemoteDocumentTests { get => Name == "Remote document"; } // horrible convention matches existing tests + + internal IEnumerable Sequence { get; private set; } + + internal string Name { get; private set; } + } +} \ No newline at end of file diff --git a/JsonLD.Infrastructure.Text.Tests/TinyJson/JSONParser.cs b/JsonLD.Infrastructure.Text.Tests/TinyJson/JSONParser.cs new file mode 100644 index 0000000..9f87afc --- /dev/null +++ b/JsonLD.Infrastructure.Text.Tests/TinyJson/JSONParser.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; + +namespace JsonLD.Infrastructure.Text.Tests.TinyJson +{ + // Really simple JSON parser in ~300 lines + // - Attempts to parse JSON files with minimal GC allocation + // - Nice and simple "[1,2,3]".FromJson>() API + // - Classes and structs can be parsed too! + // class Foo { public int Value; } + // "{\"Value\":10}".FromJson() + // - Can parse JSON without type information into Dictionary and List e.g. + // "[1,2,3]".FromJson().GetType() == typeof(List) + // "{\"Value\":10}".FromJson().GetType() == typeof(Dictionary) + // - No JIT Emit support to support AOT compilation on iOS + // - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead. + // - Only public fields and property setters on classes/structs will be written to + // + // Limitations: + // - No JIT Emit support to parse structures quickly + // - Limited to parsing <2GB JSON files (due to int.MaxValue) + // - Parsing of abstract classes or interfaces is NOT supported and will throw an exception. + internal static class JSONParser + { + [ThreadStatic] static Stack> splitArrayPool; + [ThreadStatic] static StringBuilder stringBuilder; + [ThreadStatic] static Dictionary> fieldInfoCache; + [ThreadStatic] static Dictionary> propertyInfoCache; + + public static T FromJson(this string json) + { + // Initialize, if needed, the ThreadStatic variables + if (propertyInfoCache == null) propertyInfoCache = new Dictionary>(); + if (fieldInfoCache == null) fieldInfoCache = new Dictionary>(); + if (stringBuilder == null) stringBuilder = new StringBuilder(); + if (splitArrayPool == null) splitArrayPool = new Stack>(); + + //Remove all whitespace not within strings to make parsing simpler + stringBuilder.Length = 0; + for (int i = 0; i < json.Length; i++) + { + char c = json[i]; + if (c == '"') + { + i = AppendUntilStringEnd(true, i, json); + continue; + } + if (char.IsWhiteSpace(c)) + continue; + + stringBuilder.Append(c); + } + + //Parse the thing! + return (T)ParseValue(typeof(T), stringBuilder.ToString()); + } + + static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) + { + stringBuilder.Append(json[startIdx]); + for (int i = startIdx + 1; i < json.Length; i++) + { + if (json[i] == '\\') + { + if (appendEscapeCharacter) + stringBuilder.Append(json[i]); + stringBuilder.Append(json[i + 1]); + i++;//Skip next character as it is escaped + } + else if (json[i] == '"') + { + stringBuilder.Append(json[i]); + return i; + } + else + stringBuilder.Append(json[i]); + } + return json.Length - 1; + } + + //Splits { :, : } and [ , ] into a list of strings + static List Split(string json) + { + List splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : new List(); + splitArray.Clear(); + if (json.Length == 2) + return splitArray; + int parseDepth = 0; + stringBuilder.Length = 0; + for (int i = 1; i < json.Length - 1; i++) + { + switch (json[i]) + { + case '[': + case '{': + parseDepth++; + break; + case ']': + case '}': + parseDepth--; + break; + case '"': + i = AppendUntilStringEnd(true, i, json); + continue; + case ',': + case ':': + if (parseDepth == 0) + { + splitArray.Add(stringBuilder.ToString()); + stringBuilder.Length = 0; + continue; + } + break; + } + + stringBuilder.Append(json[i]); + } + + splitArray.Add(stringBuilder.ToString()); + + return splitArray; + } + + internal static object ParseValue(Type type, string json) + { + if (type == typeof(string)) + { + if (json.Length <= 2) + return string.Empty; + StringBuilder parseStringBuilder = new StringBuilder(json.Length); + for (int i = 1; i < json.Length - 1; ++i) + { + if (json[i] == '\\' && i + 1 < json.Length - 1) + { + int j = "\"\\nrtbf/".IndexOf(json[i + 1]); + if (j >= 0) + { + parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]); + ++i; + continue; + } + if (json[i + 1] == 'u' && i + 5 < json.Length - 1) + { + UInt32 c = 0; + if (UInt32.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c)) + { + parseStringBuilder.Append((char)c); + i += 5; + continue; + } + } + } + parseStringBuilder.Append(json[i]); + } + return parseStringBuilder.ToString(); + } + if (type.IsPrimitive) + { + var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture); + return result; + } + if (type == typeof(decimal)) + { + decimal result; + decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); + return result; + } + if (json == "null") + { + return null; + } + if (type.IsEnum) + { + if (json[0] == '"') + json = json.Substring(1, json.Length - 2); + try + { + return Enum.Parse(type, json, false); + } + catch + { + return 0; + } + } + if (type.IsArray) + { + Type arrayType = type.GetElementType(); + if (json[0] != '[' || json[json.Length - 1] != ']') + return null; + + List elems = Split(json); + Array newArray = Array.CreateInstance(arrayType, elems.Count); + for (int i = 0; i < elems.Count; i++) + newArray.SetValue(ParseValue(arrayType, elems[i]), i); + splitArrayPool.Push(elems); + return newArray; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + Type listType = type.GetGenericArguments()[0]; + if (json[0] != '[' || json[json.Length - 1] != ']') + return null; + + List elems = Split(json); + var list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count }); + for (int i = 0; i < elems.Count; i++) + list.Add(ParseValue(listType, elems[i])); + splitArrayPool.Push(elems); + return list; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + Type keyType, valueType; + { + Type[] args = type.GetGenericArguments(); + keyType = args[0]; + valueType = args[1]; + } + + //Refuse to parse dictionary keys that aren't of type string + if (keyType != typeof(string)) + return null; + //Must be a valid dictionary element + if (json[0] != '{' || json[json.Length - 1] != '}') + return null; + //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON + List elems = Split(json); + if (elems.Count % 2 != 0) + return null; + + var dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 }); + for (int i = 0; i < elems.Count; i += 2) + { + if (elems[i].Length <= 2) + continue; + string keyValue = elems[i].Substring(1, elems[i].Length - 2); + object val = ParseValue(valueType, elems[i + 1]); + dictionary[keyValue] = val; + } + return dictionary; + } + if (type == typeof(object)) + { + return ParseAnonymousValue(json); + } + if (json[0] == '{' && json[json.Length - 1] == '}') + { + return ParseObject(type, json); + } + + return null; + } + + static object ParseAnonymousValue(string json) + { + if (json.Length == 0) + return null; + if (json[0] == '{' && json[json.Length - 1] == '}') + { + List elems = Split(json); + if (elems.Count % 2 != 0) + return null; + var dict = new Dictionary(elems.Count / 2); + for (int i = 0; i < elems.Count; i += 2) + dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]); + return dict; + } + if (json[0] == '[' && json[json.Length - 1] == ']') + { + List items = Split(json); + var finalList = new List(items.Count); + for (int i = 0; i < items.Count; i++) + finalList.Add(ParseAnonymousValue(items[i])); + return finalList; + } + if (json[0] == '"' && json[json.Length - 1] == '"') + { + string str = json.Substring(1, json.Length - 2); + return str.Replace("\\", string.Empty); + } + if (char.IsDigit(json[0]) || json[0] == '-') + { + if (json.Contains(".")) + { + double result; + double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); + return result; + } + else + { + int result; + int.TryParse(json, out result); + return result; + } + } + if (json == "true") + return true; + if (json == "false") + return false; + // handles json == "null" as well as invalid JSON + return null; + } + + static Dictionary CreateMemberNameDictionary(T[] members) where T : MemberInfo + { + Dictionary nameToMember = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < members.Length; i++) + { + T member = members[i]; + if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) + continue; + + string name = member.Name; + if (member.IsDefined(typeof(DataMemberAttribute), true)) + { + DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); + if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) + name = dataMemberAttribute.Name; + } + + nameToMember.Add(name, member); + } + + return nameToMember; + } + + static object ParseObject(Type type, string json) + { + object instance = FormatterServices.GetUninitializedObject(type); + + //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON + List elems = Split(json); + if (elems.Count % 2 != 0) + return instance; + + Dictionary nameToField; + Dictionary nameToProperty; + if (!fieldInfoCache.TryGetValue(type, out nameToField)) + { + nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + fieldInfoCache.Add(type, nameToField); + } + if (!propertyInfoCache.TryGetValue(type, out nameToProperty)) + { + nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); + propertyInfoCache.Add(type, nameToProperty); + } + + for (int i = 0; i < elems.Count; i += 2) + { + if (elems[i].Length <= 2) + continue; + string key = elems[i].Substring(1, elems[i].Length - 2); + string value = elems[i + 1]; + + FieldInfo fieldInfo; + PropertyInfo propertyInfo; + if (nameToField.TryGetValue(key, out fieldInfo)) + fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value)); + else if (nameToProperty.TryGetValue(key, out propertyInfo)) + propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null); + } + + return instance; + } + } +} \ No newline at end of file diff --git a/JsonLD.Infrastructure.Text.Tests/TinyJson/README.md b/JsonLD.Infrastructure.Text.Tests/TinyJson/README.md new file mode 100644 index 0000000..1c0bf45 --- /dev/null +++ b/JsonLD.Infrastructure.Text.Tests/TinyJson/README.md @@ -0,0 +1,2 @@ +Files are copy/pasted from https://github.com/zanders3/json as recommended by the author. +Original codebase is licensed as MIT \ No newline at end of file diff --git a/JsonLD.Infrastructure.Text/DocumentLoader.cs b/JsonLD.Infrastructure.Text/DocumentLoader.cs new file mode 100644 index 0000000..c3a14d4 --- /dev/null +++ b/JsonLD.Infrastructure.Text/DocumentLoader.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections; +using System.IO; +using System.Linq; +using JsonLD.Core; +using JsonLD.Util; +using System.Net; +using System.Collections.Generic; + +namespace JsonLD.Infrastructure.Text +{ + public class DocumentLoader + { + /// + public virtual RemoteDocument LoadDocument(string url) + { +#if !PORTABLE && !IS_CORECLR + RemoteDocument doc = new RemoteDocument(url, null); + HttpWebResponse resp; + + try + { + HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url); + req.Accept = AcceptHeader; + resp = (HttpWebResponse)req.GetResponse(); + bool isJsonld = resp.Headers[HttpResponseHeader.ContentType] == "application/ld+json"; + if (!resp.Headers[HttpResponseHeader.ContentType].Contains("json")) + { + throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url); + } + + string[] linkHeaders = resp.Headers.GetValues("Link"); + if (!isJsonld && linkHeaders != null) + { + linkHeaders = linkHeaders.SelectMany((h) => h.Split(",".ToCharArray())) + .Select(h => h.Trim()).ToArray(); + IEnumerable linkedContexts = linkHeaders.Where(v => v.EndsWith("rel=\"http://www.w3.org/ns/json-ld#context\"")); + if (linkedContexts.Count() > 1) + { + throw new JsonLdError(JsonLdError.Error.MultipleContextLinkHeaders); + } + string header = linkedContexts.First(); + string linkedUrl = header.Substring(1, header.IndexOf(">") - 1); + string resolvedUrl = URL.Resolve(url, linkedUrl); + var remoteContext = this.LoadDocument(resolvedUrl); + doc.contextUrl = remoteContext.documentUrl; + doc.context = remoteContext.document; + } + + doc.DocumentUrl = req.Address.ToString(); + using (var stream = resp.GetResponseStream()) + using (var reader = new StreamReader(stream)) + doc.Document = reader.ReadToEnd(); + } + catch (JsonLdError) + { + throw; + } + catch (WebException webException) + { + try + { + resp = (HttpWebResponse)webException.Response; + int baseStatusCode = (int)(Math.Floor((double)resp.StatusCode / 100)) * 100; + if (baseStatusCode == 300) + { + string location = resp.Headers[HttpResponseHeader.Location]; + if (!string.IsNullOrWhiteSpace(location)) + { + // TODO: Add recursion break or simply switch to HttpClient so we don't have to recurse on HTTP redirects. + return LoadDocument(location); + } + } + } + catch (Exception innerException) + { + throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, innerException); + } + + throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, webException); + } + catch (Exception exception) + { + throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, exception); + } + return doc; +#else + throw new PlatformNotSupportedException(); +#endif + } + + internal Core.DocumentLoader AsCore() => new Core.DocumentLoader(url => LoadDocument(url).AsCore()); + + /// An HTTP Accept header that prefers JSONLD. + /// An HTTP Accept header that prefers JSONLD. + public const string AcceptHeader = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1"; + +// private static volatile IHttpClient httpClient; + +// /// +// /// Returns a Map, List, or String containing the contents of the JSON +// /// resource resolved from the URL. +// /// +// /// +// /// Returns a Map, List, or String containing the contents of the JSON +// /// resource resolved from the URL. +// /// +// /// The URL to resolve +// /// +// /// The Map, List, or String that represent the JSON resource +// /// resolved from the URL +// /// +// /// If the JSON was not valid. +// /// +// /// If there was an error resolving the resource. +// /// +// public static object FromURL(URL url) +// { +// MappingJsonFactory jsonFactory = new MappingJsonFactory(); +// InputStream @in = OpenStreamFromURL(url); +// try +// { +// JsonParser parser = jsonFactory.CreateParser(@in); +// try +// { +// JsonToken token = parser.NextToken(); +// Type type; +// if (token == JsonToken.StartObject) +// { +// type = typeof(IDictionary); +// } +// else +// { +// if (token == JsonToken.StartArray) +// { +// type = typeof(IList); +// } +// else +// { +// type = typeof(string); +// } +// } +// return parser.ReadValueAs(type); +// } +// finally +// { +// parser.Close(); +// } +// } +// finally +// { +// @in.Close(); +// } +// } + +// /// +// /// Opens an +// /// Java.IO.InputStream +// /// for the given +// /// Java.Net.URL +// /// , including support +// /// for http and https URLs that are requested using Content Negotiation with +// /// application/ld+json as the preferred content type. +// /// +// /// The URL identifying the source. +// /// An InputStream containing the contents of the source. +// /// If there was an error resolving the URL. +// public static InputStream OpenStreamFromURL(URL url) +// { +// string protocol = url.GetProtocol(); +// if (!JsonLDNet.Shims.EqualsIgnoreCase(protocol, "http") && !JsonLDNet.Shims.EqualsIgnoreCase +// (protocol, "https")) +// { +// // Can't use the HTTP client for those! +// // Fallback to Java's built-in URL handler. No need for +// // Accept headers as it's likely to be file: or jar: +// return url.OpenStream(); +// } +// IHttpUriRequest request = new HttpGet(url.ToExternalForm()); +// // We prefer application/ld+json, but fallback to application/json +// // or whatever is available +// request.AddHeader("Accept", AcceptHeader); +// IHttpResponse response = GetHttpClient().Execute(request); +// int status = response.GetStatusLine().GetStatusCode(); +// if (status != 200 && status != 203) +// { +// throw new IOException("Can't retrieve " + url + ", status code: " + status); +// } +// return response.GetEntity().GetContent(); +// } + +// public static IHttpClient GetHttpClient() +// { +// IHttpClient result = httpClient; +// if (result == null) +// { +// lock (typeof(JSONUtils)) +// { +// result = httpClient; +// if (result == null) +// { +// // Uses Apache SystemDefaultHttpClient rather than +// // DefaultHttpClient, thus the normal proxy settings for the +// // JVM will be used +// DefaultHttpClient client = new SystemDefaultHttpClient(); +// // Support compressed data +// // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/httpagent.html#d5e1238 +// client.AddRequestInterceptor(new RequestAcceptEncoding()); +// client.AddResponseInterceptor(new ResponseContentEncoding()); +// CacheConfig cacheConfig = new CacheConfig(); +// cacheConfig.SetMaxObjectSize(1024 * 128); +// // 128 kB +// cacheConfig.SetMaxCacheEntries(1000); +// // and allow caching +// httpClient = new CachingHttpClient(client, cacheConfig); +// result = httpClient; +// } +// } +// } +// return result; +// } + +// public static void SetHttpClient(IHttpClient nextHttpClient) +// { +// lock (typeof(JSONUtils)) +// { +// httpClient = nextHttpClient; +// } +// } + } +} diff --git a/JsonLD.Infrastructure.Text/JsonLD.Infrastructure.Text.csproj b/JsonLD.Infrastructure.Text/JsonLD.Infrastructure.Text.csproj new file mode 100644 index 0000000..d97baa9 --- /dev/null +++ b/JsonLD.Infrastructure.Text/JsonLD.Infrastructure.Text.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/JsonLD.Infrastructure.Text/JsonLdOptions.cs b/JsonLD.Infrastructure.Text/JsonLdOptions.cs new file mode 100644 index 0000000..cd86db0 --- /dev/null +++ b/JsonLD.Infrastructure.Text/JsonLdOptions.cs @@ -0,0 +1,200 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace JsonLD.Infrastructure.Text +{ + public class JsonLdOptions + { + public JsonLdOptions() + { + this.SetBase(string.Empty); + } + + public JsonLdOptions(string @base) + { + this.SetBase(@base); + } + + public virtual JsonLdOptions Clone() + { + return new JsonLdOptions(GetBase()); + } + + private string @base = null; + + private bool compactArrays = true; + + private string expandContext = null; + + internal Core.JsonLdOptions AsCore() + { + var options = new Core.JsonLdOptions(GetBase()); + options.SetCompactArrays(GetCompactArrays()); + var expandContextAsString = GetExpandContext(); + if (!string.IsNullOrWhiteSpace(expandContextAsString)) + options.SetExpandContext(JObject.Parse(expandContextAsString)); + options.SetProcessingMode(GetProcessingMode()); + options.SetEmbed(GetEmbed()); + options.SetExplicit(GetExplicit()); + options.SetOmitDefault(GetOmitDefault()); + options.SetUseRdfType(GetUseRdfType()); + options.SetUseNativeTypes(GetUseNativeTypes()); + options.SetProduceGeneralizedRdf(GetProduceGeneralizedRdf()); + options.SetSortGraphsFromRdf(GetSortGraphsFromRdf()); + options.SetSortGraphNodesFromRdf(GetSortGraphNodesFromRdf()); + options.format = format; + options.useNamespaces = useNamespaces; + options.outputForm = outputForm; + options.documentLoader = documentLoader.AsCore(); + return options; + } + + private string processingMode = "json-ld-1.0"; + + private bool? embed = null; + + private bool? @explicit = null; + + private bool? omitDefault = null; + + internal bool useRdfType = false; + + internal bool useNativeTypes = false; + + private bool produceGeneralizedRdf = false; + + private bool sortGraphsFromRdf = true; + + private bool sortGraphNodesFromRdf = true; + // base options + // frame options + // rdf conversion options + public virtual bool? GetEmbed() + { + return embed; + } + + public virtual void SetEmbed(bool? embed) + { + this.embed = embed; + } + + public virtual bool? GetExplicit() + { + return @explicit; + } + + public virtual void SetExplicit(bool? @explicit) + { + this.@explicit = @explicit; + } + + public virtual bool? GetOmitDefault() + { + return omitDefault; + } + + public virtual void SetOmitDefault(bool? omitDefault) + { + this.omitDefault = omitDefault; + } + + public virtual bool GetCompactArrays() + { + return compactArrays; + } + + public virtual void SetCompactArrays(bool compactArrays) + { + this.compactArrays = compactArrays; + } + + public virtual string GetExpandContext() + { + return expandContext; + } + + public virtual void SetExpandContext(string expandContext) + { + this.expandContext = expandContext; + } + + public virtual string GetProcessingMode() + { + return processingMode; + } + + public virtual void SetProcessingMode(string processingMode) + { + this.processingMode = processingMode; + } + + public virtual string GetBase() + { + return @base; + } + + public virtual void SetBase(string @base) + { + this.@base = @base; + } + + public virtual bool GetUseRdfType() + { + return useRdfType; + } + + public virtual void SetUseRdfType(bool useRdfType) + { + this.useRdfType = useRdfType; + } + + public virtual bool GetUseNativeTypes() + { + return useNativeTypes; + } + + public virtual void SetUseNativeTypes(bool useNativeTypes) + { + this.useNativeTypes = useNativeTypes; + } + + public virtual bool GetProduceGeneralizedRdf() + { + // TODO Auto-generated method stub + return this.produceGeneralizedRdf; + } + + public virtual void SetProduceGeneralizedRdf(bool produceGeneralizedRdf) + { + this.produceGeneralizedRdf = produceGeneralizedRdf; + } + + public virtual bool GetSortGraphsFromRdf() + { + return sortGraphsFromRdf; + } + + public virtual void SetSortGraphsFromRdf(bool sortGraphs) + { + this.sortGraphsFromRdf = sortGraphs; + } + + public virtual bool GetSortGraphNodesFromRdf() + { + return sortGraphNodesFromRdf; + } + + public virtual void SetSortGraphNodesFromRdf(bool sortGraphNodes) + { + this.sortGraphNodesFromRdf = sortGraphNodes; + } + public string format = null; + + public bool useNamespaces = false; + + public string outputForm = null; + + public DocumentLoader documentLoader = new DocumentLoader(); + } +} diff --git a/JsonLD.Infrastructure.Text/JsonLdProcessor.cs b/JsonLD.Infrastructure.Text/JsonLdProcessor.cs new file mode 100644 index 0000000..41f2f15 --- /dev/null +++ b/JsonLD.Infrastructure.Text/JsonLdProcessor.cs @@ -0,0 +1,71 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JsonLD.Infrastructure.Text +{ + public class JsonLdProcessor + { + private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }; + + public static string Compact(string input, string context, JsonLdOptions options) + { + var parsedInput = AsJToken(input); + var parsedContext = AsJToken(context); + var wrappedOptions = options.AsCore(); + var processed = Core.JsonLdProcessor.Compact(parsedInput, parsedContext, wrappedOptions); + return processed.ToString(); + } + + public static string Expand(string input, JsonLdOptions options) + { + var parsedInput = AsJToken(input); + var wrappedOptions = options.AsCore(); + var processed = Core.JsonLdProcessor.Expand(parsedInput, wrappedOptions); + return processed.ToString(); + } + + public static string Flatten(string input, string context, JsonLdOptions options) + { + var parsedInput = AsJToken(input); + var parsedContext = AsJToken(context); + var wrappedOptions = options.AsCore(); + var processed = Core.JsonLdProcessor.Flatten(parsedInput, parsedContext, wrappedOptions); + return processed.ToString(); + } + + public static string Frame(string input, string frame, JsonLdOptions options) + { + var parsedInput = AsJToken(input); + var parsedFrame = AsJToken(frame); + var wrappedOptions = options.AsCore(); + var processed = Core.JsonLdProcessor.Frame(parsedInput, parsedFrame, wrappedOptions); + return processed.ToString(); + } + + public static string FromRDF(string input, JsonLdOptions options) + { + var parsedInput = (JToken)input; + var wrappedOptions = options.AsCore(); + var processed = Core.JsonLdProcessor.FromRDF(parsedInput, wrappedOptions); + return processed.ToString(); + } + + public static object Normalize(string input, JsonLdOptions options) + { + var parsedInput = AsJToken(input); + var wrappedOptions = options.AsCore(); + var processed = Core.JsonLdProcessor.Normalize(parsedInput, wrappedOptions); + return processed; + } + + public static string ToRDF(string input, JsonLdOptions options) + { + var parsedInput = AsJToken(input); + var wrappedOptions = options.AsCore(); + var processed = Core.JsonLdProcessor.ToRDF(parsedInput, wrappedOptions); + return processed.ToString(); + } + + private static JToken AsJToken(string json) => json == null ? null : JsonConvert.DeserializeObject(json, _settings); + } +} \ No newline at end of file diff --git a/JsonLD.Infrastructure.Text/RemoteDocument.cs b/JsonLD.Infrastructure.Text/RemoteDocument.cs new file mode 100644 index 0000000..4255f80 --- /dev/null +++ b/JsonLD.Infrastructure.Text/RemoteDocument.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonLD.Infrastructure.Text +{ + public class RemoteDocument + { + public virtual string DocumentUrl + { + get + { + return documentUrl; + } + set + { + this.documentUrl = value; + } + } + + public virtual string Document + { + get + { + return document; + } + set + { + this.document = value; + } + } + + public virtual string ContextUrl + { + get + { + return contextUrl; + } + set + { + this.contextUrl = value; + } + } + + public virtual string Context + { + get + { + return context; + } + set + { + this.context = value; + } + } + + internal string documentUrl; + + internal string document; + + internal string contextUrl; + + internal string context; + + public RemoteDocument(string url, string document) + : this(url, document, null) + { + } + + public RemoteDocument(string url, string document, string context) + { + this.documentUrl = url; + this.document = document; + this.contextUrl = context; + } + + internal Core.RemoteDocument AsCore() => new Core.RemoteDocument(documentUrl, JObject.Parse(document), contextUrl); + } +} diff --git a/JsonLD.sln b/JsonLD.sln index cdef61d..245b179 100644 --- a/JsonLD.sln +++ b/JsonLD.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28010.2026 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30011.22 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "json-ld.net", "src\json-ld.net\json-ld.net.csproj", "{E1AB2A29-D1E4-45A1-9076-8255916F5693}" EndProject @@ -12,9 +12,15 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8010CF28-BCB3-4DC2-901F-3118B2AAD142}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{4B8ED350-355A-4D30-9F63-B13FBFDBD9E8}" -ProjectSection(SolutionItems) = preProject - .gitignore = .gitignore -EndProjectSection + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastructure", "{9E020C9F-3649-49C5-A4FF-0A2343D5A553}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonLD.Infrastructure.Text", "JsonLD.Infrastructure.Text\JsonLD.Infrastructure.Text.csproj", "{D091ADAE-A022-4CFC-8268-C5FA089E6FD8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonLD.Infrastructure.Text.Tests", "JsonLD.Infrastructure.Text.Tests\JsonLD.Infrastructure.Text.Tests.csproj", "{C0B57654-CB3F-47AE-AB34-9B14B0C2E061}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -30,15 +36,25 @@ Global {05CBE0E2-FBD2-40D1-BD9A-D30BD7ACF219}.Debug|Any CPU.Build.0 = Debug|Any CPU {05CBE0E2-FBD2-40D1-BD9A-D30BD7ACF219}.Release|Any CPU.ActiveCfg = Release|Any CPU {05CBE0E2-FBD2-40D1-BD9A-D30BD7ACF219}.Release|Any CPU.Build.0 = Release|Any CPU + {D091ADAE-A022-4CFC-8268-C5FA089E6FD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D091ADAE-A022-4CFC-8268-C5FA089E6FD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D091ADAE-A022-4CFC-8268-C5FA089E6FD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D091ADAE-A022-4CFC-8268-C5FA089E6FD8}.Release|Any CPU.Build.0 = Release|Any CPU + {C0B57654-CB3F-47AE-AB34-9B14B0C2E061}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0B57654-CB3F-47AE-AB34-9B14B0C2E061}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0B57654-CB3F-47AE-AB34-9B14B0C2E061}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0B57654-CB3F-47AE-AB34-9B14B0C2E061}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {F10834B6-ACA3-4C86-892B-368D0B68ED83} - EndGlobalSection GlobalSection(NestedProjects) = preSolution {E1AB2A29-D1E4-45A1-9076-8255916F5693} = {8D83EC18-10BB-4C6E-A34A-AC183AD6D814} {05CBE0E2-FBD2-40D1-BD9A-D30BD7ACF219} = {8010CF28-BCB3-4DC2-901F-3118B2AAD142} + {D091ADAE-A022-4CFC-8268-C5FA089E6FD8} = {9E020C9F-3649-49C5-A4FF-0A2343D5A553} + {C0B57654-CB3F-47AE-AB34-9B14B0C2E061} = {9E020C9F-3649-49C5-A4FF-0A2343D5A553} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F10834B6-ACA3-4C86-892B-368D0B68ED83} EndGlobalSection EndGlobal diff --git a/src/json-ld.net/Core/DocumentLoader.cs b/src/json-ld.net/Core/DocumentLoader.cs index 3eb83dc..cfaf9ec 100644 --- a/src/json-ld.net/Core/DocumentLoader.cs +++ b/src/json-ld.net/Core/DocumentLoader.cs @@ -14,6 +14,10 @@ public class DocumentLoader /// public virtual RemoteDocument LoadDocument(string url) { + if (_documentLoader != null) + { + return _documentLoader(url); + } #if !PORTABLE && !IS_CORECLR RemoteDocument doc = new RemoteDocument(url, null); HttpWebResponse resp; @@ -93,6 +97,12 @@ public virtual RemoteDocument LoadDocument(string url) /// An HTTP Accept header that prefers JSONLD. public const string AcceptHeader = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1"; + private Func _documentLoader; + + public DocumentLoader() { } + + public DocumentLoader(Func documentLoader) => _documentLoader = documentLoader; + // private static volatile IHttpClient httpClient; // /// diff --git a/src/json-ld.net/Core/JsonLdUtils.cs b/src/json-ld.net/Core/JsonLdUtils.cs index 0ac976e..7cbbd56 100644 --- a/src/json-ld.net/Core/JsonLdUtils.cs +++ b/src/json-ld.net/Core/JsonLdUtils.cs @@ -2,9 +2,10 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using JsonLD.Core; +using RegularExpressions = System.Text.RegularExpressions; using JsonLD.Util; using Newtonsoft.Json.Linq; +using Newtonsoft.Json; namespace JsonLD.Core { @@ -135,6 +136,32 @@ public static bool DeepCompare(JToken v1, JToken v2, bool listOrderMatters) } } + public static bool DeepCompareStrings(string v1, string v2) => DeepCompare(JsonOrXMLAsJToken(v1), JsonOrXMLAsJToken(v2)); + + private static readonly RegularExpressions.Regex _likelyJSON = new RegularExpressions.Regex(@"(^\{.*\}$)|(^\[.*\]$)", RegularExpressions.RegexOptions.Singleline); + + /* + * The existing code compares strings of XML or triples by comparing JToken strings! As we don't already know whether this is "real" string of + * JSON or merely a string of some other description which will be compared as a JSON string, we need to sniff the data to work out how to parse it + */ + private static JToken JsonOrXMLAsJToken(string v1) + { + var probablyJSON = _likelyJSON.IsMatch(v1.Trim()); + if(probablyJSON) + { + try + { + return JToken.Parse(v1); + } + catch(JsonReaderException) + { + // in case our "likely JSON" turns out to be something not parseable after all... + return v1; + } + } return v1; + + } + public static bool DeepCompare(JToken v1, JToken v2) { return DeepCompare(v1, v2, false);