From f221686deb5912f06a0c503854951761d1b19d75 Mon Sep 17 00:00:00 2001 From: Marko Urh Date: Mon, 26 Jun 2023 16:49:57 +0200 Subject: [PATCH] minor: Optional keep flag (#6) * test push * minor: add optional keep flag to keep attribute * fix: trie access modifiers --- .github/workflows/publish.yml | 1 + source/Directory.Build.props | 2 +- source/Perun.Differ.Tests/DifferTests.cs | 52 +++++++ .../TestTypes/KeepAttributeModels.cs | 16 ++ source/Perun.Differ/AttributeApplier.cs | 81 +++++----- source/Perun.Differ/Attributes.cs | 31 ++-- source/Perun.Differ/DifferDotNet.cs | 15 +- source/Perun.Differ/Difference.cs | 10 +- source/Perun.Differ/Extensions.cs | 11 ++ source/Perun.Differ/Trie.cs | 143 ++++++++++++++++++ 10 files changed, 294 insertions(+), 68 deletions(-) create mode 100644 source/Perun.Differ/Trie.cs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f96848d..e19279b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,7 @@ on: push: branches: - main + workflow_dispatch: jobs: build-and-publish: diff --git a/source/Directory.Build.props b/source/Directory.Build.props index 2ac9372..d7297b1 100644 --- a/source/Directory.Build.props +++ b/source/Directory.Build.props @@ -1,7 +1,7 @@ - 2.0.3 + 2.0.4-optional-keep Marko Urh Perun Copyright (c) 2023 Marko Urh and other authors. diff --git a/source/Perun.Differ.Tests/DifferTests.cs b/source/Perun.Differ.Tests/DifferTests.cs index 496a49c..b76a3e1 100644 --- a/source/Perun.Differ.Tests/DifferTests.cs +++ b/source/Perun.Differ.Tests/DifferTests.cs @@ -442,6 +442,54 @@ public void NestedComplexIterable_Diffs() Assert.Equal(expectedPropsInDiffCount, actualPropsInDiffCount); } + [Fact] + public void OptionalKeepDiff_Simple_NoSiblingChanges_Ignores() + { + var faker = new AutoFaker(); + var left = faker.UseSeed(1).Generate(); + + var diff = DifferDotNet.Diff(left, left).SingleOrDefault(); + + Assert.Null(diff); + } + + [Fact] + public void OptionalKeepDiff_Simple_SiblingChanges_DoesNotIgnore() + { + var faker = new AutoFaker(); + var left = faker.UseSeed(1).Generate(); + var right = faker.UseSeed(2).Generate(); + + var diffs = DifferDotNet.Diff(left, right); + + Assert.Equal(2, diffs.Count()); + } + + [Fact] + public void OptionalKeepDiff_Complex_NoSiblingChange_Ignores() + { + var faker = new AutoFaker(); + var left = faker.UseSeed(1).Generate(); + + var diff = DifferDotNet.Diff(left, left).SingleOrDefault(); + + Assert.Null(diff); + } + + [Fact] + public void OptionalKeepDiff_Complex_ChildChange_DoesNotIgnore() + { + var faker = new AutoFaker(); + var left = faker.UseSeed(1).Generate(); + var right = faker.UseSeed(2).Generate(); + + right.NoDiff = left.NoDiff; + + var diff = DifferDotNet.Diff(left, right); + + Assert.NotNull(diff.SingleOrDefault()); + } + [Fact] public void KeepDiff_Simple_Keeps() { @@ -453,6 +501,7 @@ public void KeepDiff_Simple_Keeps() Assert.Equal(left.NoDiffKeepMe, diff.LeftValue); Assert.Equal(left.NoDiffKeepMe, diff.RightValue); Assert.Equal(diff.LeftValue, diff.RightValue); + Assert.False(diff.IgnoreIfNoOtherDiff); } [Fact] @@ -464,6 +513,7 @@ public void KeepDiff_IterableSimple_KeepsAllChildren() var diffs = DifferDotNet.Diff(left, left).ToList(); Assert.Equal(left.NoDiffKeepMe.Count(), diffs.Count); + Assert.True(diffs.TrueForAll(x => !x.IgnoreIfNoOtherDiff)); } [Fact] @@ -475,6 +525,7 @@ public void KeepDiff_IterableComplex_KeepsAllChildren() var diffs = DifferDotNet.Diff(left, left).ToList(); Assert.Equal(left.NoDiffKeepMe.Count(), diffs.Count); + Assert.True(diffs.TrueForAll(x => !x.IgnoreIfNoOtherDiff)); } [Fact] @@ -487,6 +538,7 @@ public void KeepDiff_Complex_KeepsAllChildren() var expectedDiffCount = left.NoDiffKeepMe.GetType().GetProperties().Length; Assert.Equal(expectedDiffCount, diffs.Count); + Assert.True(diffs.TrueForAll(x => !x.IgnoreIfNoOtherDiff)); } [Fact] diff --git a/source/Perun.Differ.Tests/TestTypes/KeepAttributeModels.cs b/source/Perun.Differ.Tests/TestTypes/KeepAttributeModels.cs index 5e69efb..98ab7e2 100644 --- a/source/Perun.Differ.Tests/TestTypes/KeepAttributeModels.cs +++ b/source/Perun.Differ.Tests/TestTypes/KeepAttributeModels.cs @@ -33,4 +33,20 @@ public class ComplexKeepModel public ComplexType NoDiff { get; set; } } + + public class SimpleOptionalKeepModel + { + [KeepInDiff(IgnoreIfNoOtherDiff = true)] + public string NoDiffKeepMe { get; set; } + + public string NoDiff { get; set; } + } + + public class ComplexOptionalKeepModel + { + [KeepInDiff(IgnoreIfNoOtherDiff = true)] + public ComplexType NoDiffKeepMe { get; set; } + + public ComplexType NoDiff { get; set; } + } } \ No newline at end of file diff --git a/source/Perun.Differ/AttributeApplier.cs b/source/Perun.Differ/AttributeApplier.cs index 2f28e31..c4b9530 100644 --- a/source/Perun.Differ/AttributeApplier.cs +++ b/source/Perun.Differ/AttributeApplier.cs @@ -7,75 +7,64 @@ internal static class AttributeApplier { internal static DiffCollection ApplyAttributes(DiffCollection collection) { - var keepPaths = collection.KeepPaths.ToList(); - var ignorePaths = collection.IgnorePaths.ToList(); + var trie = CreateTrie(collection); - AddDiffsToKeep(collection.Diffs, collection.KeepDiffs); - RemoveDiffsToIgnore(collection.Diffs, ignorePaths, keepPaths); + AddDiffsToKeep(collection.Diffs, collection.KeepDiffs, trie); + RemoveDiffsToIgnore(collection.Diffs, collection.IgnorePaths); return collection; } - private static Dictionary AddDiffsToKeep( - Dictionary differences, - List keepDiffs - ) + private static Trie CreateTrie(DiffCollection collection) { - foreach (var keepDiff in keepDiffs) + var hasOptional = collection.KeepDiffs.Any(x => x.IgnoreIfNoOtherDiff); + var hasIgnore = collection.IgnorePaths.Any(); + + var trie = new Trie(); + if (hasOptional || hasIgnore) { - // relevant, keep it in diff - if (!differences.ContainsKey(keepDiff.FullPath)) + foreach (var diff in collection.Diffs.Values) { - differences.Add(keepDiff.FullPath, keepDiff); + trie.Add(diff.FieldPath, diff); } } - return differences; + return trie; } - private static Dictionary RemoveDiffsToIgnore( + private static void AddDiffsToKeep( Dictionary differences, - List ignorePaths, - List keepPaths - ) + HashSet keepDiffs, + Trie trie + ) { - var ignoreKeys = new List(); - foreach (var ignorePath in ignorePaths) + foreach (var diff in keepDiffs) { - var ignorePathSplit = ignorePath.Split('.'); - - ignoreKeys.AddRange( - differences.Keys.Where(key => - { - var keySplit = key.Split('.'); - - var startsWith = true; - for (var i = 0; i < ignorePathSplit.Length; i++) - { - if (i >= keySplit.Length) - { - break; - } - - var ignoreNode = ignorePathSplit[i]; - var keyNode = keySplit[i]; + if (diff.IgnoreIfNoOtherDiff && !trie.Retrieve(diff.FieldPath).Any()) + { + continue; + } - startsWith &= ignoreNode?.ToLower() == keyNode?.ToLower(); - } + var added = differences.TryAdd(diff.FullPath, diff); - return startsWith; - }) - ); + if (added) // modify trie for search on new records + { + trie.Add(diff.FullPath, diff); + } } + } - ignoreKeys = ignoreKeys.Except(keepPaths).ToList(); + private static void RemoveDiffsToIgnore( + Dictionary differences, + HashSet ignorePaths + ) + { + var pathsToRemove = ignorePaths.Where(differences.ContainsKey); - foreach (var ignoreKey in ignoreKeys) + foreach (var path in pathsToRemove) { - differences.Remove(ignoreKey); + differences.Remove(path); } - - return differences; } } } \ No newline at end of file diff --git a/source/Perun.Differ/Attributes.cs b/source/Perun.Differ/Attributes.cs index 6357b0e..ab5990a 100644 --- a/source/Perun.Differ/Attributes.cs +++ b/source/Perun.Differ/Attributes.cs @@ -8,16 +8,24 @@ namespace Differ.DotNet /// Keeps property and subsequent children in audit diff even if no change was made. /// [PublicAPI] - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property)] public sealed class KeepInDiffAttribute : Attribute - { + { + /// + /// Gets or sets a value indicating whether attribute should be ignored if sibling or child diffs exist. + /// + /// + /// If true attribute is ignored (not kept), if no sibling or child diffs exist. + /// If false values are always kept. + /// + public bool IgnoreIfNoOtherDiff { get; set; } } /// /// Ignores property and subsequent children in audit diff. /// [PublicAPI] - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property)] public sealed class IgnoreInDiffAttribute : Attribute { } @@ -27,7 +35,7 @@ public sealed class IgnoreInDiffAttribute : Attribute /// /// [PublicAPI] - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property)] public sealed class DiffPropertyName : Attribute { public string Name { get; } @@ -38,21 +46,20 @@ public DiffPropertyName(string name) } } - [Flags] + [Flags] internal enum DiffActions { Default = 0, - Keep = 1, - Ignore = 2 + Keep = 1, + Ignore = 2, + KeepOptional = 4, } internal sealed class DiffCollection { - public Dictionary Diffs { get; set; } = new Dictionary(); - - public List KeepDiffs { get; set; } = new List(); - public HashSet KeepPaths { get; set; } = new HashSet(); + public Dictionary Diffs { get; set; } = new(); // (FullPath, Diff) - public HashSet IgnorePaths { get; set; } = new HashSet(); + public HashSet KeepDiffs { get; set; } = new(); + public HashSet IgnorePaths { get; set; } = new(); // FullPath } } \ No newline at end of file diff --git a/source/Perun.Differ/DifferDotNet.cs b/source/Perun.Differ/DifferDotNet.cs index 7bd00de..fc1494e 100644 --- a/source/Perun.Differ/DifferDotNet.cs +++ b/source/Perun.Differ/DifferDotNet.cs @@ -94,12 +94,11 @@ DiffActions actions } // Check for KeepInDiff attribute - if (prop.GetCustomAttribute() != null) + if (prop.GetCustomAttribute() is { } keepAtt) { - if (!diffs.KeepPaths.Contains(fullPath)) - diffs.KeepPaths.Add(fullPath); - - actions |= DiffActions.Keep; + actions |= keepAtt.IgnoreIfNoOtherDiff + ? DiffActions.KeepOptional + : DiffActions.Keep; } // Recurse on sub-objects @@ -176,11 +175,13 @@ DiffActions actions rightObj ); - if (actions.HasFlag(DiffActions.Keep)) + if (actions.HasFlag(DiffActions.Keep) || actions.HasFlag(DiffActions.KeepOptional)) { + diff.IgnoreIfNoOtherDiff = actions.HasFlag(DiffActions.KeepOptional); + diff.Keep = true; diffs.KeepDiffs.Add(diff); } - + var exitCond = actions.HasFlag(DiffActions.Ignore) || leftObj == null && rightObj == null || diffs.Diffs.ContainsKey(diff.FullPath) diff --git a/source/Perun.Differ/Difference.cs b/source/Perun.Differ/Difference.cs index 86b025b..e164d0c 100644 --- a/source/Perun.Differ/Difference.cs +++ b/source/Perun.Differ/Difference.cs @@ -18,7 +18,7 @@ public sealed class Difference public string CustomFieldName { get; set; } public object LeftValue { get; set; } - public object RightValue { get; set; } + public object RightValue { get; set; } public Difference() { @@ -48,6 +48,12 @@ public Difference(string fullPath, string customFullPath, object leftValue, obje var fieldPath = string.Join(".", pathSplit.Take(pathSplit.Length - 1)); return (fullPath, fieldName, fieldPath); - } + } + + /// assembly internals + + internal bool IgnoreIfNoOtherDiff { get; set; } + internal bool Keep { get; set; } + internal bool Ignore { get; set; } } } \ No newline at end of file diff --git a/source/Perun.Differ/Extensions.cs b/source/Perun.Differ/Extensions.cs index 9b76371..c68ee4a 100644 --- a/source/Perun.Differ/Extensions.cs +++ b/source/Perun.Differ/Extensions.cs @@ -9,6 +9,17 @@ namespace Differ.DotNet { internal static class Extensions { + internal static bool TryAdd(this Dictionary dict, TKey key, TValue value) + { + if (dict.ContainsKey(key)) + { + dict.Add(key, value); + return true; + } + + return false; + } + /// /// Reference: https://stackoverflow.com/a/65079923 /// diff --git a/source/Perun.Differ/Trie.cs b/source/Perun.Differ/Trie.cs new file mode 100644 index 0000000..d9289fc --- /dev/null +++ b/source/Perun.Differ/Trie.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Differ.DotNet +{ + [Serializable] + internal sealed class Trie : TrieNode + { + public IEnumerable Retrieve(string query) + { + return Retrieve(query, 0); + } + + public void Add(string key, TValue value) + { + Add(key, 0, value); + } + } + + [Serializable] + internal class TrieNode : TrieNodeBase + { + private readonly Dictionary> m_Children; + private readonly Queue m_Values; + + protected TrieNode() + { + m_Children = new Dictionary>(); + m_Values = new Queue(); + } + + protected override int KeyLength + { + get { return 1; } + } + + protected override IEnumerable> Children() + { + return m_Children.Values; + } + + protected override IEnumerable Values() + { + return m_Values; + } + + protected override TrieNodeBase GetOrCreateChild(char key) + { + TrieNode result; + if (!m_Children.TryGetValue(key, out result)) + { + result = new TrieNode(); + m_Children.Add(key, result); + } + return result; + } + + protected override TrieNodeBase GetChildOrNull(string query, int position) + { + if (query == null) throw new ArgumentNullException("query"); + TrieNode childNode; + return + m_Children.TryGetValue(query[position], out childNode) + ? childNode + : null; + } + + protected override void AddValue(TValue value) + { + m_Values.Enqueue(value); + } + } + + [Serializable] + internal abstract class TrieNodeBase + { + protected abstract int KeyLength { get; } + + protected abstract IEnumerable Values(); + + protected abstract IEnumerable> Children(); + + public long Size() + { + return Children().Sum(o => o.Size()) + 1; + } + + public void Add(string key, int position, TValue value) + { + if (key == null) throw new ArgumentNullException("key"); + if (EndOfString(position, key)) + { + AddValue(value); + return; + } + + var child = GetOrCreateChild(key[position]); + child.Add(key, position + 1, value); + } + + protected abstract void AddValue(TValue value); + + protected abstract TrieNodeBase GetOrCreateChild(char key); + + protected virtual IEnumerable Retrieve(string query, int position) + { + return + EndOfString(position, query) + ? ValuesDeep() + : SearchDeep(query, position); + } + + protected virtual IEnumerable SearchDeep(string query, int position) + { + var nextNode = GetChildOrNull(query, position); + return nextNode != null + ? nextNode.Retrieve(query, position + nextNode.KeyLength) + : Enumerable.Empty(); + } + + protected abstract TrieNodeBase GetChildOrNull(string query, int position); + + private static bool EndOfString(int position, string text) + { + return position >= text.Length; + } + + private IEnumerable ValuesDeep() + { + return + Subtree() + .SelectMany(node => node.Values()); + } + + protected IEnumerable> Subtree() + { + return + Enumerable.Repeat(this, 1) + .Concat(Children().SelectMany(child => child.Subtree())); + } + } +} \ No newline at end of file