Skip to content

Conversation

@Valian
Copy link
Contributor

@Valian Valian commented Jul 21, 2025

I've added a new option to diff letting to customize structs before diffing them. By default it does nothing, but can be used to nicely customize diff behaviour for your structs.

I've built that PR on top of #33, so that other one should be merged first.

Fixes #29

Valian added 5 commits July 21, 2025 12:57
Enhance the diff function to accept an `:ancestor_path` option, allowing users to specify a starting point for diffing nested maps and lists. Update related tests to verify functionality with various ancestor paths, including handling of escaped characters.
…uct handling

Updated the diff function to accept a `:prepare_struct` option, allowing users to define a function for converting structs to maps. This change includes modifications to related functions and tests to ensure proper functionality with various struct scenarios.
…truct handling

Added detailed documentation for the `:prepare_struct` option, which allows users to define a function for converting structs to maps during the diff process. Updated the default behavior to a no-op function for clarity.
@corka149 corka149 self-requested a review July 22, 2025 08:16
@corka149 corka149 added this to the 2.3 milestone Jul 22, 2025
@Valian
Copy link
Contributor Author

Valian commented Jul 22, 2025

@corka149 I've noticed one missing thing - I forgot to process add and remove patches, now it's fixed :) Once you'll decide it's ready to merge, I'll rebase #32 on top of all these changes.

@corka149
Copy link
Owner

@Valian looks good. But what do you think about a prepare_target and prepare_source in general and not only limited to structs?

@Valian
Copy link
Contributor Author

Valian commented Jul 22, 2025

I agree we could make it work for any data. I suggested structs since they're easy to pattern match, and usually should be enough.

Not sure about different transforms for source and destination. Do you have an use case for different transforms for source and destination?

@corka149
Copy link
Owner

When someone is using regular maps which should be filtered for passwords or api tokens.

Or someone wants a "ignore case" for strings and wants to lower case all strings before diffing.

Do you think there is any disadvantage to allowing such functionality for any type?🤔

@Valian
Copy link
Contributor Author

Valian commented Jul 22, 2025

The only disadvantage I see is potentially a small performance hit - instead of doing it only for structs we would be doing it for each value, including strings, numbers etc. Unless I misunderstood and you'd like to do it only for maps and not for individual simple values (strings etc) inside maps?

Do you think there is any disadvantage to allowing such functionality for any type?🤔

I only expressed concern about two separate handlers, prepare_source and prepare_target. Preparing them in a different way seems very niche and I can't really think about a use case when you'd like to prepare source in a different way than target.

In other words, for me prepare_struct is an optimization. Everyone can quite easily code their own mapper of source and destination, run it and then call Jsonpatch.diff with resulting values:

source = prepare_for_diff(source)
destination = prepare_for_diff(destination)
Jsonpatch.diff(source, destination)

The difference is in:

  1. Convenience - one less thing to write
  2. Performance - we delay transformation until really needed, when we know given values are really equal.

I think library API should cover 90% of the most common, reasonable requirements. With prepare_struct I wanted to cover my own need, and it seems reasonable to me. It's quite similar to what eg Jason.encode does, you can customize how structs will be dumped into JSON but you cannot do it for regular maps.


After a lengthy response, here what I'd suggest:

  • let's rename prepare_struct to prepare_value (or prepare_map?)
  • let's run it for both regular maps and structs
  • if someone would need to eg transform all strings or numbers in some way, it's trivial to write prepare_value to do it.

What do you think @corka149?

@corka149
Copy link
Owner

Sounds reasonable @Valian . I agree with your suggestions. Let's use prepare_map and focus on maps and structs.

@Valian
Copy link
Contributor Author

Valian commented Jul 23, 2025

Nice! Will adjust soon

@Valian
Copy link
Contributor Author

Valian commented Jul 28, 2025

Sorry for it taking some time @corka149.

New approach is almost ready. Just, I realized now that function has to almost always include a passthrough clause eg

test "Create diff with prepare_map option using subset of fields" do
      source = %TestStruct{
        field1: "value1",
        field2: "value2",
        inner: %{nested: "old"},
        field: "ignored"
      }

      destination = %TestStruct{
        field1: "new_value",
        field2: "value2",
        inner: %{nested: "new"},
        field: "also_ignored"
      }

      patches =
        Jsonpatch.diff(source, destination,
          prepare_map: fn
            %TestStruct{field1: field1, inner: inner} -> %{field1: field1, inner: inner}
            map -> map
          end
        )

      expected_patches = [
        %{op: "replace", path: "/field1", value: "new_value"},
        %{op: "replace", path: "/inner/nested", value: "new"}
      ]

      assert_equal_patches(patches, expected_patches)
    end

If I'd write simply &Map.take(&1, [:field1, :inner]) it wouldn't work, because it would be applied also to nested values effectively clearing them up. But I guess it's fine 👍🏻

Valian added 2 commits July 28, 2025 18:52
Implemented logic to create a replace operation when the type of the root value changes, such as setting a value to nil. Updated tests to verify this behavior, including cases with ancestor paths.
@Valian
Copy link
Contributor Author

Valian commented Jul 28, 2025

@corka149 I've added one more change here. When I was using Jsonpatch in live_vue, especially with ancestor_path specified, I was not getting expected replace path when type of value was changed.

You can check my latest commit, but in short I'm now generating a replace operation when type of root value was updated. I think it makes sense? According to AI, this is correct based on RFC.

    test "Create full replace operation when type of root value changes" do
      assert [%{op: "replace", path: "", value: 1}] = Jsonpatch.diff("unexpected", 1)
    end

corka149
corka149 previously approved these changes Jul 28, 2025
@Valian Valian dismissed corka149’s stale review July 28, 2025 18:53

The merge-base changed after approval.

@corka149
Copy link
Owner

Thx for all the effort @Valian 🙏

@corka149 corka149 merged commit 0a67aad into corka149:master Jul 28, 2025
10 checks passed
@Valian
Copy link
Contributor Author

Valian commented Jul 28, 2025

no problem! Now onto rebasing #32 and hopefully merge & release <3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Customize diffs for structs

2 participants