Skip to content

Commit 1a6049e

Browse files
committed
Attach original json file path to json token to be able to distinguish between template and definition source
1 parent bbd82a3 commit 1a6049e

29 files changed

Lines changed: 549 additions & 313 deletions

shared/Json/JsonExtensions.cs

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Text;
6+
using Dibix.Sdk.Abstractions;
7+
using Newtonsoft.Json;
8+
using Newtonsoft.Json.Linq;
9+
10+
namespace Dibix.Sdk
11+
{
12+
internal static class JsonExtensions
13+
{
14+
public static void SetFileSource(this JToken json, string filePath)
15+
{
16+
json.AddAnnotation(new JsonFileSourceAnnotation(filePath));
17+
}
18+
public static void SetFileSource(this JContainer json, string filePath)
19+
{
20+
foreach (JToken token in json.DescendantsAndSelf())
21+
SetFileSource(token, filePath);
22+
}
23+
24+
public static JsonSourceInfo GetSourceInfo(this JToken token)
25+
{
26+
string filePath = (token.Annotation<JsonFileSourceAnnotation>() ?? token.Root.Annotation<JsonFileSourceAnnotation>())?.FilePath;
27+
28+
IJsonLineInfo lineInfo = token;
29+
30+
bool hasLineInfo = lineInfo.HasLineInfo();
31+
int lineNumber = lineInfo.LineNumber;
32+
int linePosition = lineInfo.LinePosition;
33+
34+
if (hasLineInfo)
35+
{
36+
switch (token)
37+
{
38+
case JValue value:
39+
linePosition = value.GetCorrectLinePosition();
40+
break;
41+
42+
case JProperty property:
43+
linePosition = property.GetCorrectLinePosition();
44+
break;
45+
}
46+
}
47+
48+
return new JsonSourceInfo(filePath, lineNumber, linePosition);
49+
}
50+
51+
public static T Merge<T>(this T source, T content) where T : JContainer
52+
{
53+
if (content == null)
54+
return source;
55+
56+
// We don't want our content to overwrite the source, if a property has been explicitly set.
57+
// We could inverse source and content and do 'content.Merge(source)'.
58+
// That would however modify the root document.
59+
// For example in a template use case, the action definition root will now be the one of the global template.
60+
//object contentCopy = content.DeepClone();
61+
//source.Merge(contentCopy);
62+
63+
JsonMerger<JContainer>.Merge(source, content);
64+
65+
return source;
66+
}
67+
68+
// The line positions are somewhat weird and unexpected
69+
// Not sure if this is a bug, but we have to adjust the position to get the actual start of the value
70+
private static int GetCorrectLinePosition(this JValue value)
71+
{
72+
IJsonLineInfo lineInfo = value;
73+
StringBuilder sb = new StringBuilder();
74+
using (TextWriter textWriter = new System.IO.StringWriter(sb))
75+
{
76+
using (JsonWriter jsonWriter = new JsonTextWriter(textWriter))
77+
{
78+
value.WriteTo(jsonWriter);
79+
int valueEnd = lineInfo.LinePosition + 1;
80+
int result = valueEnd - sb.Length;
81+
82+
// And while we're at it anyways, we can skip ahead the " just for convenience
83+
if (value.Type == JTokenType.String)
84+
result++;
85+
86+
return result;
87+
}
88+
}
89+
}
90+
private static int GetCorrectLinePosition(this JProperty property)
91+
{
92+
IJsonLineInfo lineInfo = property;
93+
int result = lineInfo.LinePosition - 1 - property.Name.Length;
94+
return result;
95+
}
96+
}
97+
98+
internal sealed class JsonSourceInfo
99+
{
100+
public string FilePath { get; }
101+
public int LineNumber { get; }
102+
public int LinePosition { get; }
103+
104+
public JsonSourceInfo(string filePath, int lineNumber, int linePosition)
105+
{
106+
FilePath = filePath;
107+
LineNumber = lineNumber;
108+
LinePosition = linePosition;
109+
}
110+
}
111+
112+
internal abstract class JsonMerger<T> where T : JContainer
113+
{
114+
public static void Merge(JContainer container, object content, JsonMergeSettings settings = null)
115+
{
116+
switch (container)
117+
{
118+
case JArray array:
119+
new JsonArrayMerger().MergeContent(array, content, settings);
120+
break;
121+
122+
case JConstructor ctor:
123+
new JsonConstructorMerger().MergeContent(ctor, content, settings);
124+
break;
125+
126+
case JObject obj:
127+
// This is the main reason we are not using Newtonsoft's Merge function
128+
const bool replaceExistingProperties = false;
129+
new JObjectMerger(replaceExistingProperties).MergeContent(obj, content, settings);
130+
break;
131+
132+
case JProperty property:
133+
new JPropertyMerger().MergeContent(property, content, settings);
134+
break;
135+
136+
default:
137+
throw new ArgumentOutOfRangeException(nameof(container));
138+
}
139+
}
140+
141+
public abstract void MergeContent(T container, object content, JsonMergeSettings settings);
142+
143+
private static void MergeEnumerableContent(JContainer target, IEnumerable content, JsonMergeSettings settings)
144+
{
145+
switch (settings?.MergeArrayHandling ?? MergeArrayHandling.Concat)
146+
{
147+
case MergeArrayHandling.Concat:
148+
foreach (JToken item in content)
149+
target.Add(item);
150+
151+
break;
152+
153+
case MergeArrayHandling.Union:
154+
HashSet<JToken> items = new HashSet<JToken>(target, JToken.EqualityComparer);
155+
foreach (JToken item in content)
156+
{
157+
if (items.Add(item))
158+
target.Add(item);
159+
}
160+
break;
161+
162+
case MergeArrayHandling.Replace:
163+
if (Equals(target, content))
164+
break;
165+
166+
((ICollection<JToken>)target).Clear();
167+
foreach (JToken item in content)
168+
target.Add(item);
169+
170+
break;
171+
172+
case MergeArrayHandling.Merge:
173+
int i = 0;
174+
foreach (object targetItem in content)
175+
{
176+
if (i < target.Count)
177+
{
178+
JToken sourceItem = target[i];
179+
if (sourceItem is JContainer existingContainer)
180+
{
181+
Merge(existingContainer, targetItem, settings);
182+
}
183+
else
184+
{
185+
if (targetItem != null)
186+
{
187+
JToken contentValue = CreateFromContent(targetItem);
188+
if (contentValue.Type != JTokenType.Null)
189+
{
190+
target[i] = contentValue;
191+
}
192+
}
193+
}
194+
}
195+
else
196+
{
197+
target.Add(targetItem);
198+
}
199+
200+
i++;
201+
}
202+
break;
203+
204+
default:
205+
throw new ArgumentOutOfRangeException(nameof(settings), "Unexpected merge array handling when merging JSON.");
206+
}
207+
}
208+
209+
private static JToken CreateFromContent(object content) => content as JToken ?? new JValue(content);
210+
211+
private sealed class JsonArrayMerger : JsonMerger<JArray>
212+
{
213+
public override void MergeContent(JArray container, object content, JsonMergeSettings settings)
214+
{
215+
IEnumerable enumerableContent = IsMultiContent(content) || content is JArray ? (IEnumerable)content : null;
216+
217+
if (enumerableContent == null)
218+
return;
219+
220+
MergeEnumerableContent(container, enumerableContent, settings);
221+
}
222+
223+
private static bool IsMultiContent(object content) => content is IEnumerable and not string and not JToken and not byte[];
224+
}
225+
226+
private sealed class JsonConstructorMerger : JsonMerger<JConstructor>
227+
{
228+
public override void MergeContent(JConstructor container, object content, JsonMergeSettings settings)
229+
{
230+
if (content is not JConstructor contentCtor)
231+
return;
232+
233+
if (contentCtor.Name != null)
234+
container.Name = contentCtor.Name;
235+
236+
MergeEnumerableContent(container, contentCtor, settings);
237+
}
238+
}
239+
240+
private sealed class JObjectMerger : JsonMerger<JObject>
241+
{
242+
private readonly bool _replaceExistingProperties;
243+
244+
public JObjectMerger(bool replaceExistingProperties = true)
245+
{
246+
_replaceExistingProperties = replaceExistingProperties;
247+
}
248+
249+
public override void MergeContent(JObject container, object content, JsonMergeSettings settings)
250+
{
251+
if (content is not JObject contentObj)
252+
return;
253+
254+
foreach (KeyValuePair<string, JToken> contentItem in contentObj)
255+
{
256+
JProperty existingProperty = container.Property(contentItem.Key, settings?.PropertyNameComparison ?? StringComparison.Ordinal);
257+
258+
if (existingProperty == null)
259+
{
260+
container.Add(contentItem.Key, contentItem.Value);
261+
}
262+
else if (contentItem.Value != null)
263+
{
264+
if (existingProperty.Value is not JContainer existingContainer || existingContainer.Type != contentItem.Value.Type)
265+
{
266+
bool isNull = IsNull(contentItem.Value);
267+
if (isNull && settings?.MergeNullValueHandling == MergeNullValueHandling.Merge
268+
|| !isNull && _replaceExistingProperties)
269+
{
270+
existingProperty.Value = contentItem.Value;
271+
}
272+
}
273+
else
274+
{
275+
Merge(existingContainer, contentItem.Value, settings);
276+
}
277+
}
278+
}
279+
}
280+
281+
private static bool IsNull(JToken token) => token.Type == JTokenType.Null || token is JValue { Value: null };
282+
}
283+
284+
private sealed class JPropertyMerger : JsonMerger<JProperty>
285+
{
286+
public override void MergeContent(JProperty container, object content, JsonMergeSettings settings)
287+
{
288+
JToken value = (content as JProperty)?.Value;
289+
290+
if (value != null && value.Type != JTokenType.Null)
291+
{
292+
container.Value = value;
293+
}
294+
}
295+
}
296+
}
297+
}

shared/Json/JsonSchemaDefinitionReader.cs renamed to shared/Json/ValidatingJsonDefinitionReader.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99

1010
namespace Dibix.Sdk
1111
{
12-
internal abstract class JsonSchemaDefinitionReader
12+
internal abstract class ValidatingJsonDefinitionReader
1313
{
1414
public bool HasSchemaErrors { get; private set; }
1515
protected IFileSystemProvider FileSystemProvider { get; }
1616
protected ILogger Logger { get; }
1717
protected abstract string SchemaName { get; }
1818

19-
protected JsonSchemaDefinitionReader(IFileSystemProvider fileSystemProvider, ILogger logger)
19+
protected ValidatingJsonDefinitionReader(IFileSystemProvider fileSystemProvider, ILogger logger)
2020
{
2121
this.FileSystemProvider = fileSystemProvider;
2222
this.Logger = logger;
@@ -33,6 +33,7 @@ protected void Collect(IEnumerable<string> inputs)
3333
using (JsonReader jsonReader = new JsonTextReader(textReader))
3434
{
3535
JObject json = JObject.Load(jsonReader/*, new JsonLoadSettings { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error }*/);
36+
json.SetFileSource(filePath);
3637

3738
if (!json.IsValid(JsonSchemaDefinition.GetSchema(this.GetType().Assembly, this.SchemaName), out IList<ValidationError> errors))
3839
{
@@ -45,13 +46,13 @@ protected void Collect(IEnumerable<string> inputs)
4546
continue;
4647
}
4748

48-
this.Read(filePath, json);
49+
this.Read(json);
4950
}
5051
}
5152
}
5253
}
5354
}
5455

55-
protected abstract void Read(string filePath, JObject json);
56+
protected abstract void Read(JObject json);
5657
}
5758
}

src/Dibix.Sdk.Abstractions/IUserConfigurationReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ namespace Dibix.Sdk.Abstractions
44
{
55
public interface IUserConfigurationReader
66
{
7-
void Read(string filePath, JObject json);
7+
void Read(JObject json);
88
}
99
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Dibix.Sdk.Abstractions
2+
{
3+
public sealed class JsonFileSourceAnnotation
4+
{
5+
public string FilePath { get; }
6+
7+
public JsonFileSourceAnnotation(string filePath)
8+
{
9+
FilePath = filePath;
10+
}
11+
}
12+
}

src/Dibix.Sdk.CodeAnalysis/SqlCodeAnalysisUserConfigurationReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public SqlCodeAnalysisUserConfigurationReader(SqlCodeAnalysisConfiguration confi
1212
_configuration = configuration;
1313
}
1414

15-
public void Read(string filePath, JObject json)
15+
public void Read(JObject json)
1616
{
1717
const string sqlCodeAnalysisConfigurationName = "SqlCodeAnalysis";
1818
JObject sqlCodeAnalysisConfiguration = (JObject)json.Property(sqlCodeAnalysisConfigurationName)?.Value;

0 commit comments

Comments
 (0)