Skip to content

Commit

Permalink
Merge pull request #57 from Cratis:generalizing-changes
Browse files Browse the repository at this point in the history
Generalizing the Changes API
  • Loading branch information
einari authored Nov 8, 2021
2 parents 65447da + 237d465 commit 401f29d
Show file tree
Hide file tree
Showing 65 changed files with 324 additions and 285 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

using System.Dynamic;

namespace Cratis.Events.Projections.Changes
namespace Cratis.Changes
{
/// <summary>
/// Defines a change as part of a <see cref="Changeset"/>.
/// Defines a change as part of a <see cref="Changeset{T}"/>.
/// </summary>
/// <param name="State">State after change applied.</param>
public record Change(ExpandoObject State);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,30 @@

using System.Dynamic;
using Cratis.Dynamic;
using Cratis.Properties;

namespace Cratis.Events.Projections.Changes
namespace Cratis.Changes
{
/// <summary>
/// Represents changes that can be applied to a <see cref="Changeset"/>.
/// Represents changes that can be applied to a <see cref="Changeset{T}"/>.
/// </summary>
public static class Changes
{
/// <summary>
/// Applies properties to the <see cref="Changeset"/>.
/// Applies properties to the <see cref="Changeset{T}"/>.
/// </summary>
/// <param name="changeset"><see cref="Changeset"/> to apply to.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper">property mappers</see> that will manipulate properties on the target.</param>
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper{T}">property mappers</see> that will manipulate properties on the target.</param>
/// <remarks>
/// This will run a diff against the initial state and only produce changes that are new.
/// </remarks>
public static void ApplyProperties(this Changeset changeset, IEnumerable<PropertyMapper> propertyMappers)
public static void ApplyProperties<T>(this Changeset<T> changeset, IEnumerable<PropertyMapper<T>> propertyMappers)
{
var workingState = changeset.InitialState.Clone();
foreach (var propertyMapper in propertyMappers)
{
propertyMapper(changeset.Event, workingState);
propertyMapper(changeset.Incoming, workingState);
}

var comparer = new ObjectsComparer.Comparer<ExpandoObject>();
Expand All @@ -35,29 +37,30 @@ public static void ApplyProperties(this Changeset changeset, IEnumerable<Propert
}

/// <summary>
/// Applies properties for a child to the <see cref="Changeset"/>.
/// Applies properties for a child to the <see cref="Changeset{T}"/>.
/// </summary>
/// <param name="changeset"><see cref="Changeset"/> to apply to.</param>
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
/// <param name="item">The item to add from.</param>
/// <param name="childrenProperty">The <see cref="Property"/> on the parent that holds the children.</param>
/// <param name="identifiedByProperty">The <see cref="Property"/> on the instance that identifies the child.</param>
/// <param name="keyResolver">The <see cref="EventValueProvider"/> for resolving the key on the event.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper">property mappers</see> that will manipulate properties on the target.</param>
/// <param name="keyResolver">The <see cref="ValueProvider{T}"/> for resolving the key on the event.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper{T}">property mappers</see> that will manipulate properties on the target.</param>
/// <remarks>
/// This will run a diff against the initial state and only produce changes that are new.
/// </remarks>
public static void ApplyChildProperties(
this Changeset changeset,
public static void ApplyChildProperties<T>(
this Changeset<T> changeset,
ExpandoObject item,
Property childrenProperty,
Property identifiedByProperty,
EventValueProvider keyResolver,
IEnumerable<PropertyMapper> propertyMappers)
ValueProvider<T> keyResolver,
IEnumerable<PropertyMapper<T>> propertyMappers)
{
var workingItem = item.Clone();
foreach (var propertyMapper in propertyMappers)
{
propertyMapper(changeset.Event, workingItem);
propertyMapper(changeset.Incoming, workingItem);
}

var comparer = new ObjectsComparer.Comparer<ExpandoObject>();
Expand All @@ -67,26 +70,27 @@ public static void ApplyChildProperties(
workingItem,
childrenProperty,
identifiedByProperty,
keyResolver(changeset.Event),
keyResolver(changeset.Incoming),
differences.Select(_ => new PropertyDifference(item, workingItem, _))));
}
}

/// <summary>
/// Applies properties to the child in the model to the <see cref="Changeset"/>.
/// Applies properties to the child in the model to the <see cref="Changeset{T}"/>.
/// </summary>
/// <param name="changeset"><see cref="Changeset"/> to apply to.</param>
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
/// <param name="childrenProperty"><see cref="Property"/> for accessing the children collection.</param>
/// <param name="identifiedByProperty"><see cref="Property"/> that identifies the child.</param>
/// <param name="key">Key value.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper">property mappers</see> that will manipulate properties on the target.</param>
/// <param name="propertyMappers">Collection of <see cref="PropertyMapper{T}">property mappers</see> that will manipulate properties on the target.</param>
/// <exception cref="ChildrenPropertyIsNotEnumerable">Thrown when children property is not enumerable.</exception>
public static void ApplyAddChild(
this Changeset changeset,
public static void ApplyAddChild<T>(
this Changeset<T> changeset,
Property childrenProperty,
Property identifiedByProperty,
object key,
IEnumerable<PropertyMapper> propertyMappers)
IEnumerable<PropertyMapper<T>> propertyMappers)
{
var workingState = changeset.InitialState.Clone();
var items = workingState.EnsureCollection(childrenProperty);
Expand All @@ -97,7 +101,7 @@ public static void ApplyAddChild(

foreach (var propertyMapper in propertyMappers)
{
propertyMapper(changeset.Event, item);
propertyMapper(changeset.Incoming, item);
}

identifiedByProperty.SetValue(item, key);
Expand All @@ -108,18 +112,20 @@ public static void ApplyAddChild(
}

/// <summary>
/// Apply a remove change to the <see cref="Changeset"/>.
/// Apply a remove change to the <see cref="Changeset{T}"/>.
/// </summary>
/// <param name="changeset"><see cref="Changeset"/> to apply to.</param>
public static void ApplyRemove(this Changeset changeset)
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
public static void ApplyRemove<T>(this Changeset<T> changeset)
{
}

/// <summary>
/// Apply a remove child change to the <see cref="Changeset"/>.
/// Apply a remove child change to the <see cref="Changeset{T}"/>.
/// </summary>
/// <param name="changeset"><see cref="Changeset"/> to apply to.</param>
public static void ApplyRemoveChild(this Changeset changeset)
/// <typeparam name="T">Type the changeset is for.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to apply to.</param>
public static void ApplyRemoveChild<T>(this Changeset<T> changeset)
{
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,31 @@

using System.Dynamic;

namespace Cratis.Events.Projections.Changes
namespace Cratis.Changes
{
/// <summary>
/// Represents a changeset - the consequence of an individual handling of a <see cref="IProjection"/>.
/// Represents a changeset of changes that can occur to an object.
/// </summary>
public class Changeset
/// <typeparam name="T">Type of object we're working on.</typeparam>
public class Changeset<T>
{
readonly List<Change> _changes = new();

/// <summary>
/// Initializes a new instance of <see cref="Changeset"/>.
/// Initializes a new instance of <see cref="Changeset{T}"/>.
/// </summary>
/// <param name="event"><see cref="Event"/> that the <see cref="Changeset"/> is for.</param>
/// <param name="incoming"><see cref="Incoming"/> that the <see cref="Changeset{T}"/> is for.</param>
/// <param name="initialState">The initial state before any changes are applied.</param>
public Changeset(Event @event, ExpandoObject initialState)
public Changeset(T incoming, ExpandoObject initialState)
{
Event = @event;
Incoming = incoming;
InitialState = initialState;
}

/// <summary>
/// Gets the <see cref="Event"/> the <see cref="Changeset"/> is for.
/// Gets the <see cref="Incoming"/> the <see cref="Changeset{T}"/> is for.
/// </summary>
public Event Event { get; }
public T Incoming { get; }

/// <summary>
/// Gets the initial state of before changes in changeset occurred.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;
using Cratis.Dynamic;
using Cratis.Properties;

namespace Cratis.Events.Projections.Changes
namespace Cratis.Changes
{
/// <summary>
/// Extension methods for working with <see cref="Changeset"/>.
/// Extension methods for working with <see cref="Changeset{T}"/>.
/// </summary>
public static class ChangesetExtensions
{
/// <summary>
/// Check if changeset contains a <see cref="ChildAdded"/> to a collection with a specific key.
/// </summary>
/// <param name="changeset"><see cref="Changeset"/> to check.</param>
/// <typeparam name="T">Type of object for the changeset.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to check.</param>
/// <param name="childrenProperty">The <see cref="Property"/> representing the collection.</param>
/// <param name="key">The key of the item.</param>
/// <returns>True if it has, false it not.</returns>
public static bool HasChildBeenAddedWithKey(this Changeset changeset, Property childrenProperty, object key)
public static bool HasChildBeenAddedWithKey<T>(this Changeset<T> changeset, Property childrenProperty, object key)
{
return changeset.Changes
.Select(_ => _ as ChildAdded)
Expand All @@ -27,12 +30,13 @@ public static bool HasChildBeenAddedWithKey(this Changeset changeset, Property c
/// <summary>
/// Get a specific child from
/// </summary>
/// <param name="changeset"><see cref="Changeset"/> to get from.</param>
/// <typeparam name="T">Type of object for the changeset.</typeparam>
/// <param name="changeset"><see cref="Changeset{T}"/> to get from.</param>
/// <param name="childrenProperty">The <see cref="Property"/> representing the collection.</param>
/// <param name="identifiedByProperty">The <see cref="Property"/> that identifies the child</param>
/// <param name="key">The key of the item.</param>
/// <returns>The added child.</returns>
public static ExpandoObject GetChildByKey(this Changeset changeset, Property childrenProperty, Property identifiedByProperty, object key)
public static ExpandoObject GetChildByKey<T>(this Changeset<T> changeset, Property childrenProperty, Property identifiedByProperty, object key)
{
var items = childrenProperty.GetValue(changeset.InitialState) as IEnumerable<ExpandoObject>;
return items!.FindByKey(identifiedByProperty, key)!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;
using Cratis.Properties;

namespace Cratis.Events.Projections.Changes
namespace Cratis.Changes
{
/// <summary>
/// Represents a child that has been added to a parent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;
using Cratis.Properties;

namespace Cratis.Events.Projections.Changes
namespace Cratis.Changes
{
/// <summary>
/// Represents properties that has been changed on a child.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System.Dynamic;

namespace Cratis.Events.Projections.Changes
namespace Cratis.Changes
{
/// <summary>
/// Represents properties that has been changed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Reflection;
using ObjectsComparer;

namespace Cratis.Events.Projections.Changes
namespace Cratis.Changes
{
/// <summary>
/// Represents a value difference in a property of an object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;
using Cratis.Events.Projections.Changes;

namespace Cratis.Events.Projections
namespace Cratis.Changes
{
/// <summary>
/// Represents an entry being removed.
Expand Down
78 changes: 78 additions & 0 deletions Source/Fundamentals/Dynamic/ExpandoObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections;
using System.Dynamic;
using Cratis.Concepts;
using Cratis.Properties;

namespace Cratis.Dynamic
{
Expand Down Expand Up @@ -95,5 +97,81 @@ public static ExpandoObject OverwriteWith(this ExpandoObject left, ExpandoObject

return result;
}

/// <summary>
/// Ensure a specific path for a <see cref="Property"/> exists on an <see cref="ExpandoObject"/>..
/// </summary>
/// <param name="target">Target <see cref="ExpandoObject"/>.</param>
/// <param name="property"><see cref="Property"/> to get or create for.</param>
/// <returns><see cref="ExpandoObject"/> at property.</returns>
public static ExpandoObject EnsurePath(this ExpandoObject target, Property property)
{
var currentTarget = target as IDictionary<string, object>;
for (var propertyIndex = 0; propertyIndex < property.Segments.Length - 1; propertyIndex++)
{
var segment = property.Segments[propertyIndex];
if (!currentTarget.ContainsKey(segment))
{
var nested = new ExpandoObject();
currentTarget[segment] = nested;
currentTarget = nested!;
}
else
{
currentTarget = ((ExpandoObject)currentTarget[segment])!;
}
}

return (currentTarget as ExpandoObject)!;
}

/// <summary>
/// Ensures that a collection exists for a specific <see cref="Property"/>.
/// </summary>
/// <param name="target">Target <see cref="ExpandoObject"/>.</param>
/// <param name="childrenProperty"><see cref="Property"/> to ensure collection for.</param>
/// <returns>The ensured <see cref="ICollection{ExpandoObject}"/>.</returns>
/// <exception cref="ChildrenPropertyIsNotEnumerable">Thrown if there is an existing property and it is not enumerable.</exception>
public static ICollection<ExpandoObject> EnsureCollection(this ExpandoObject target, Property childrenProperty)
{
var inner = target.EnsurePath(childrenProperty) as IDictionary<string, object>;
if (!inner.ContainsKey(childrenProperty.LastSegment))
{
inner[childrenProperty.LastSegment] = new List<ExpandoObject>();
}

if (!(inner[childrenProperty.LastSegment] is IEnumerable))
{
throw new ChildrenPropertyIsNotEnumerable(childrenProperty);
}

var items = (inner[childrenProperty.LastSegment] as IEnumerable)!.Cast<ExpandoObject>();
if (items is not IList<ExpandoObject>)
{
items = new List<ExpandoObject>(items!);
}
inner[childrenProperty.LastSegment] = items;
return (items as ICollection<ExpandoObject>)!;
}

/// <summary>
/// Check if there is an item with a specific key in a collection of <see cref="ExpandoObject"/> items.
/// </summary>
/// <param name="items">Items to check.</param>
/// <param name="identityProperty"><see cref="Property"/> holding identity on each item.</param>
/// <param name="key">The key value to check for.</param>
/// <returns>True if there is an item, false if not</returns>
public static bool Contains(this IEnumerable<ExpandoObject> items, Property identityProperty, object key) =>
items!.Any((IDictionary<string, object> _) => _.ContainsKey(identityProperty.Path) && _[identityProperty.Path].Equals(key));

/// <summary>
/// Find an item in a collection by its identity.
/// </summary>
/// <param name="items">Items to find from.</param>
/// <param name="identityProperty"><see cref="Property"/> holding identity on each item.</param>
/// <param name="key">The key value to check for.</param>
/// <returns>The item or default if not found.</returns>
public static ExpandoObject? FindByKey(this IEnumerable<ExpandoObject> items, Property identityProperty, object key) =>
items!.FirstOrDefault((IDictionary<string, object> _) => _.ContainsKey(identityProperty.Path) && _[identityProperty.Path].Equals(key)) as ExpandoObject;
}
}
Loading

0 comments on commit 401f29d

Please sign in to comment.