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+ }
0 commit comments