public void ApplyInverseComplexCollectionPatch() { var initial = new TestPerson() { FirstName = "John", LastName = "Doe", IsAdult = true, Cars = new List <Car>() { new Car { Make = "Ferrari", Model = "250 LM", }, new Car { Make = "Shelby", Model = "Daytona Cobra Coupe", }, new Car() { Make = "Rolls Royce", Model = "10 HP", }, new Car() { Make = "Mercedes-Benz", Model = "38/250 SSK", }, }, }; var patches = new JsonPatchDocument(); var inversePatches = new JsonPatchDocument(); ITestPerson testPerson; using (DraftScope scope = (DraftScope)DraftExtensions.CreateDraft(initial, out TestPerson draft)) { var patchGenerator = new CollectionPatchGenerator(new DynamicLargestCommonSubsequence()); draft.Cars.RemoveAt(3); draft.Cars.RemoveAt(0); draft.Cars.Add(new Car() { Make = "Bugatti", Model = "Type 57 SC Atalante", }); // trick the scope into thinking that is finishing and should not create proxies anymore. scope.IsFinishing = true; patchGenerator.Generate((IDraft)draft.Cars, "/Cars", patches, inversePatches); // inverse order of inverse patches. inversePatches.Operations.Reverse(); testPerson = scope.FinishDraft <ITestPerson, TestPerson>(draft); } var result = ITestPerson.Produce(initial, p => { patches.ApplyTo(p); }); result = result.Produce(p => { inversePatches.ApplyTo(p); }); Assert.Equal(4, result.Cars.Count); Assert.Equal("Ferrari", result.Cars[0].Make); Assert.Equal("250 LM", result.Cars[0].Model); Assert.Equal("Shelby", result.Cars[1].Make); Assert.Equal("Daytona Cobra Coupe", result.Cars[1].Model); Assert.Equal("Rolls Royce", result.Cars[2].Make); Assert.Equal("10 HP", result.Cars[2].Model); Assert.Equal("Mercedes-Benz", result.Cars[3].Make); Assert.Equal("38/250 SSK", result.Cars[3].Model); }
public void ApplyTrivialCollectionRemovalPatch() { var initial = new TestPerson() { FirstName = "John", LastName = "Doe", IsAdult = true, Cars = new List <Car>() { new Car { Make = "Ferrari", Model = "250 LM", }, new Car { Make = "Shelby", Model = "Daytona Cobra Coupe", }, new Car() { Make = "Rolls Royce", Model = "10 HP", }, new Car() { Make = "Mercedes-Benz", Model = "38/250 SSK", }, new Car() { Make = "Bugatti", Model = "Type 57 SC Atalante", }, }, }; var patches = new JsonPatchDocument(); var inversePatches = new JsonPatchDocument(); ITestPerson testPerson; using (var scope = DraftExtensions.CreateDraft(initial, out TestPerson draft)) { var patchGenerator = new CollectionPatchGenerator(new DynamicLargestCommonSubsequence()); draft.Cars.RemoveAt(2); draft.Cars.RemoveAt(2); draft.Cars.RemoveAt(2); patchGenerator.Generate((IDraft)draft.Cars, "/Cars", patches, inversePatches); // inverse order of inverse patches. inversePatches.Operations.Reverse(); testPerson = scope.FinishDraft <ITestPerson, TestPerson>(draft); } // inverse order of inverse patches. inversePatches.Operations.Reverse(); var result = ITestPerson.Produce(initial, p => { patches.ApplyTo(p); }); Assert.Equal(2, result.Cars.Count); Assert.Equal("Ferrari", result.Cars[0].Make); Assert.Equal("250 LM", result.Cars[0].Model); Assert.Equal("Shelby", result.Cars[1].Make); Assert.Equal("Daytona Cobra Coupe", result.Cars[1].Model); }
public void GenerateComplexCollectionPatch() { var initial = new TestPerson() { FirstName = "John", LastName = "Doe", IsAdult = true, Cars = new List <Car>() { new Car { Make = "Ferrari", Model = "250 LM", }, new Car { Make = "Shelby", Model = "Daytona Cobra Coupe", }, new Car() { Make = "Rolls Royce", Model = "10 HP", }, new Car() { Make = "Mercedes-Benz", Model = "38/250 SSK", }, }, }; using var scope = DraftExtensions.CreateDraft(initial, out TestPerson draft); var patchGenerator = new CollectionPatchGenerator(new DynamicLargestCommonSubsequence()); var patches = new JsonPatchDocument(); var inversePatches = new JsonPatchDocument(); draft.Cars.RemoveAt(3); draft.Cars.RemoveAt(0); draft.Cars.Add(new Car() { Make = "Bugatti", Model = "Type 57 SC Atalante", }); patchGenerator.Generate((IDraft)draft.Cars, "/Cars", patches, inversePatches); // inverse order of inverse patches. inversePatches.Operations.Reverse(); JsonAssert.Equal( @" [ { 'path': '/Cars/3', 'op': 'remove' }, { 'path': '/Cars/0', 'op': 'remove' }, { 'value': { 'Make': 'Bugatti', 'Model': 'Type 57 SC Atalante', 'Crashed': false }, 'path': '/Cars/-', 'op': 'add' } ] ", JsonConvert.SerializeObject(patches)); JsonAssert.Equal( @" [ { 'path': '/Cars/2', 'op': 'remove' }, { 'value': { 'Make': 'Ferrari', 'Model': '250 LM', 'Crashed': false }, 'path': '/Cars/0', 'op': 'add' }, { 'value': { 'Make': 'Mercedes-Benz', 'Model': '38/250 SSK', 'Crashed': false }, 'path': '/Cars/-', 'op': 'add' } ] ", JsonConvert.SerializeObject(inversePatches)); }
/// <summary> /// Finishes an instance. /// </summary> /// <param name="draft">The instance to finish.</param> /// <returns>The immutable variant of the instance.</returns> private object?FinishInstance(object?draft) { ObjectPatchGenerator? objectPatchGenerator = null; DictionaryPatchGenerator?dictionaryPatchGenerator = null; CollectionPatchGenerator?collectionPatchGenerator = null; JsonPatchDocument? patches = null; JsonPatchDocument? inversePatches = null; if (this.Parent?.Patches != null && this.Patches == null) { patches = new JsonPatchDocument(); inversePatches = new JsonPatchDocument(); } else { patches = this.Patches; inversePatches = this.InversePatches; } if (patches != null && inversePatches != null) { objectPatchGenerator = new ObjectPatchGenerator(); dictionaryPatchGenerator = new DictionaryPatchGenerator(); collectionPatchGenerator = new CollectionPatchGenerator(new DynamicLargestCommonSubsequence()); } object?Reconcile(object?draft) { if (draft == null) { return(null); } var draftType = draft.GetType(); if (draftType.IsValueType || this.AllowedImmutableReferenceTypes.Contains(draftType)) { return(draft); } var proxyType = GetProxyType(draft); if (proxyType == null) { throw new DraftException(draft, $"The object of type {draftType} cannot be made immutable."); } if (draft is IDraft idraft && this.drafts.Contains(draft)) { var delayedOperations = new List <Action>(); if (idraft.DraftState is ObjectDraftState objectDraftState) { foreach ((string propertyName, object child) in objectDraftState.ChildDrafts) { var immutable = Reconcile(child); if (ReferenceEquals(immutable, child)) { delayedOperations.Add(() => { // use reflection to set the property and trigger changed on the parent. draftType.GetProperty(propertyName).SetValue(draft, immutable); }); } } objectPatchGenerator?.Generate(idraft, idraft.DraftState !.Path !.ToString(), patches !, inversePatches !); } else if (idraft.DraftState is CollectionDraftState collectionDraftState) { if (draft is IDictionary dictionary) { foreach (DictionaryEntry entry in dictionary) { if (InternalIsDraft(entry.Value) && this.drafts.Contains(entry.Value)) { var immutable = Reconcile(entry.Value); delayedOperations.Add(() => { // draft turned into immutable. if (ReferenceEquals(immutable, entry.Value)) { idraft.DraftState !.Changed = true; } // draft reverted to original. else { dictionary[entry.Key] = immutable; } }); } } dictionaryPatchGenerator?.Generate(idraft, idraft.DraftState !.Path !.ToString(), patches !, inversePatches !); } else if (draft is IList list) { // todo: handle sets. for (int i = 0; i < list.Count; i++) { object?child = list[i]; if (InternalIsDraft(child) && this.drafts.Contains(child)) { var immutable = Reconcile(child); // capture i int captured = i; delayedOperations.Add(() => { // draft turned into immutable. if (ReferenceEquals(immutable, child)) { idraft.DraftState !.Changed = true; } // draft reverted to original. else { list[captured] = immutable; } }); } } collectionPatchGenerator?.Generate(idraft, idraft.DraftState !.Path !.ToString(), patches !, inversePatches !); } } foreach (var toExecute in delayedOperations) { toExecute(); } // not changed, return the original. if (!idraft.DraftState !.Changed) { draft = idraft.DraftState.GetOriginal <object?>(); } } return(draft); } this.IsFinishing = true; try { draft = Reconcile(draft); if (draft is ILockable lockable) { lockable.Lock(); } if (this.Parent?.Patches != null) { this.Parent.Patches.Operations.AddRange(patches !.Operations); this.Parent.InversePatches !.Operations.AddRange(inversePatches !.Operations); if (draft != null) { this.Parent.HasPatches.Add(draft); } } else { this.producerOptions.InversePatches?.Operations.Reverse(); } return(draft); } finally { this.IsFinishing = false; this.Dispose(); } }