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));
        }
Example #4
0
        /// <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();
            }
        }