diff --git a/.editorconfig b/.editorconfig
index ac225590..cffab408 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -11,5 +11,5 @@ indent_size = 2
[*.{rb,js}]
max_line_length = 80
-[README.md]
+[*.md]
max_line_length = unset
diff --git a/.github/workflows/super_diff.yml b/.github/workflows/super_diff.yml
index 25048ffe..0e144821 100644
--- a/.github/workflows/super_diff.yml
+++ b/.github/workflows/super_diff.yml
@@ -7,33 +7,31 @@ on:
pull_request:
types:
- opened
+ - closed
+ - reopened
- synchronize
concurrency:
group: build-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
- all:
- runs-on: ubuntu-latest
- needs:
- - lint
- - audit
- - test
- outputs:
- PASSED: ${{ steps.set-output.outputs.PASSED }}
- steps:
- - name: Set PASSED output
- id: set-output
- run: echo "PASSED=true" >> "$GITHUB_OUTPUT"
- lint:
+ analyze:
runs-on: ubuntu-latest
+ if: ${{ github.event_name == 'push' || github.event.action != 'closed' }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
+ - name: Download actionlint
+ id: download-actionlint
+ run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/7fdc9630cc360ea1a469eed64ac6d78caeda1234/scripts/download-actionlint.bash) 1.6.23
+ shell: bash
+ - name: Check workflow files
+ run: ${{ steps.download-actionlint.outputs.executable }} -color
+ shell: bash
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Use Node.js
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "yarn"
@@ -41,45 +39,13 @@ jobs:
run: yarn --immutable
- name: Lint
run: yarn lint
- audit:
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- ruby:
- - "3.0"
- - "3.1"
- - "3.2"
- rails_appraisal:
- - rails_6_1
- - rails_7_0
- - no_rails
- rspec_appraisal:
- - rspec_lt_3_10
- - rspec_gte_3_10
- env:
- BUNDLE_GEMFILE: gemfiles/${{ matrix.rails_appraisal }}_${{ matrix.rspec_appraisal }}.gemfile
- steps:
- - uses: actions/checkout@v3
- - name: Set up Ruby
- uses: ruby/setup-ruby@v1
- with:
- ruby-version: ${{ matrix.ruby }}
- bundler-cache: true
- - name: Use Node.js
- uses: actions/setup-node@v3
- with:
- node-version-file: ".nvmrc"
- cache: "yarn"
- - name: Install Yarn dependencies
- run: yarn --immutable
- name: Audit
run: yarn audit
+
test:
needs:
- - lint
- - audit
- runs-on: ubuntu-20.04
+ - analyze
+ runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
@@ -87,6 +53,7 @@ jobs:
- "3.0"
- "3.1"
- "3.2"
+ - "3.3"
rails_appraisal:
- rails_6_1
- rails_7_0
@@ -97,7 +64,7 @@ jobs:
env:
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails_appraisal }}_${{ matrix.rspec_appraisal }}.gemfile
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
@@ -116,3 +83,193 @@ jobs:
log-output-if: failure
- name: Run tests
run: bundle exec rake --trace
+
+ ready-to-merge:
+ runs-on: ubuntu-latest
+ needs:
+ - analyze
+ - test
+ steps:
+ - run: echo "Analysis and tests passed. Ready to merge."
+
+ collect-release-info:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+ - name: Run command
+ id: command
+ run: scripts/collect-release-info.rb
+ outputs:
+ IS_NEW_RELEASE: ${{ steps.command.outputs.IS_NEW_RELEASE }}
+ RELEASE_VERSION: ${{ steps.command.outputs.RELEASE_VERSION }}
+
+ collect-docsite-release-info:
+ runs-on: ubuntu-latest
+ needs:
+ - collect-release-info
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Run command
+ id: command
+ run: |
+ set -x
+
+ if [[ "$GITHUB_EVENT_NAME" == "push" && "$GITHUB_REF_NAME" == "main" && "$IS_NEW_RELEASE" == "true" ]]; then
+ DOCSITE_RELEASE_VERSION="$RELEASE_VERSION"
+ DOCSITE_DESTINATION_PATH="releases/$RELEASE_VERSION"
+ HAS_DOCS_CHANGES_TO_RELEASE="true"
+ else
+ DOCSITE_RELEASE_VERSION="$COMMIT_ID"
+ DOCSITE_DESTINATION_PATH="branches/$BRANCH_NAME/$COMMIT_ID"
+ # Check if there any changes to docs/
+ if git diff --quiet --merge-base "origin/$GITHUB_BASE_REF" -- docs; then
+ HAS_DOCS_CHANGES_TO_RELEASE="false"
+ else
+ HAS_DOCS_CHANGES_TO_RELEASE="true"
+ fi
+ fi
+
+ {
+ echo "DOCSITE_RELEASE_VERSION=$DOCSITE_RELEASE_VERSION"
+ echo "DOCSITE_DESTINATION_PATH=$DOCSITE_DESTINATION_PATH"
+ echo "HAS_DOCS_CHANGES_TO_RELEASE=$HAS_DOCS_CHANGES_TO_RELEASE"
+ } >> "$GITHUB_OUTPUT"
+ env:
+ IS_NEW_RELEASE: ${{ needs.collect-release-info.outputs.IS_NEW_RELEASE }}
+ RELEASE_VERSION: ${{ needs.collect-release-info.outputs.RELEASE_VERSION }}
+ BRANCH_NAME: ${{ github.head_ref }}
+ COMMIT_ID: ${{ github.event.pull_request.head.sha }}
+ outputs:
+ DOCSITE_RELEASE_VERSION: ${{ steps.command.outputs.DOCSITE_RELEASE_VERSION }}
+ DOCSITE_DESTINATION_PATH: ${{ steps.command.outputs.DOCSITE_DESTINATION_PATH }}
+ HAS_DOCS_CHANGES_TO_RELEASE: ${{ steps.command.outputs.HAS_DOCS_CHANGES_TO_RELEASE }}
+
+ build-docsite:
+ runs-on: ubuntu-latest
+ needs:
+ - analyze
+ - collect-release-info
+ - collect-docsite-release-info
+ if: ${{ needs.collect-docsite-release-info.outputs.HAS_DOCS_CHANGES_TO_RELEASE == 'true' }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install poetry
+ run: pipx install poetry
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ - name: Install Python dependencies
+ run: poetry install
+ - name: Build docsite
+ run: poetry run mkdocs build
+ - name: Save site/ for later jobs
+ uses: actions/cache/save@v3
+ with:
+ path: site
+ key: docsite-${{ github.sha }}
+
+ publish-docsite:
+ runs-on: ubuntu-latest
+ needs:
+ - collect-release-info
+ - collect-docsite-release-info
+ # This already runs if there are docs changes to publish, so we don't need
+ # to check that here
+ - build-docsite
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: gh-pages
+ - name: Restore cache from previous job
+ uses: actions/cache/restore@v3
+ with:
+ path: site
+ key: docsite-${{ github.sha }}
+ - name: Update redirect in index (for a release)
+ if: ${{ needs.collect-release-info.outputs.IS_NEW_RELEASE == 'true' }}
+ run: |
+ url="https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY#"${GITHUB_REPOSITORY_OWNER}/"}/releases/${DOCSITE_RELEASE_VERSION}"
+ cat <<-EOT > index.html
+
+
+
+ SuperDiff Documentation
+
+
+
+
+ This page has moved to a different URL.
+ Please click
+
+ this link
+
+ if you are not redirected.
+
+
+
+ EOT
+ env:
+ DOCSITE_RELEASE_VERSION: ${{ needs.collect-docsite-release-info.outputs.DOCSITE_RELEASE_VERSION }}
+ - name: Copy site/ to ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+ run: |
+ mkdir -p "$(dirname "$DOCSITE_DESTINATION_PATH")"
+ mv site "$DOCSITE_DESTINATION_PATH"
+ env:
+ DOCSITE_DESTINATION_PATH: ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+ - name: Publish new version of docsite
+ run: |
+ git add -A .
+ git config user.name "${GITHUB_ACTOR}"
+ git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
+ git commit -m "Publish docs at $DOCSITE_DESTINATION_PATH"
+ git push
+ env:
+ DOCSITE_DESTINATION_PATH: ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+ - name: Announce publishing of docsite as a comment on the PR
+ if: ${{ github.event_name == 'pull_request' }}
+ run: |
+ gh pr comment "$PULL_REQUEST_NUMBER" --body ":book: A new version of the docsite has been published at: "
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PULL_REQUEST_NUMBER: ${{ github.event.number }}
+ DOCSITE_DESTINATION_PATH: ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+
+ unpublish_docsite:
+ runs-on: ubuntu-latest
+ needs:
+ - collect-release-info
+ - collect-docsite-release-info
+ if: ${{ github.event_name == 'pull_request' && needs.collect-release-info.outputs.IS_NEW_RELEASE == 'false' && github.event.action == 'closed' }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: gh-pages
+ - name: Set DOCSITE_DESTINATION_PARENT_PATH
+ run: |
+ set -x
+ DOCSITE_DESTINATION_PARENT_PATH="$(dirname "$DOCSITE_DESTINATION_PATH")"
+ echo "DOCSITE_DESTINATION_PARENT_PATH=$DOCSITE_DESTINATION_PARENT_PATH" >> "$GITHUB_ENV"
+ env:
+ DOCSITE_DESTINATION_PATH: ${{ needs.collect-docsite-release-info.outputs.DOCSITE_DESTINATION_PATH }}
+ - name: Remove ${{ env.DOCSITE_DESTINATION_PARENT_PATH }} on gh-pages
+ run: |
+ set -x
+ if [[ "$DOCSITE_DESTINATION_PARENT_PATH" == "releases" || "$DOCSITE_DESTINATION_PARENT_PATH" == "branches" ]]; then
+ echo "Not removing $DOCSITE_DESTINATION_PARENT_PATH."
+ exit 1
+ fi
+ rm -rf "$DOCSITE_DESTINATION_PARENT_PATH"
+ - name: Re-push docsite if necessary
+ run: |
+ git add -A .
+ if ! git diff --cached --quiet; then
+ git config user.name "${GITHUB_ACTOR}"
+ git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
+ git commit -m "Remove $DOCSITE_DESTINATION_PARENT_PATH"
+ git push
+ fi
diff --git a/.gitignore b/.gitignore
index 87d7767b..fed4404a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,9 @@ zeus.server-start.log
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+
+# Ignore Python stuff
+poetry.lock
+
+# Ignore mkdocs stuff
+site
diff --git a/.husky/pre-push b/.husky/pre-push
index 3fcd9e8a..ac05d0af 100755
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -9,22 +9,7 @@ if echo "$stdin" | grep -q "^(delete)"; then
exit 0
fi
-current_branch_name="$(git branch --show-current)"
-
-if [[ "$current_branch_name" == "main" ]]; then
- raw_files_to_check="$(git diff origin/main...HEAD --name-only --diff-filter=d)"
-else
- raw_files_to_check="$(git diff main...HEAD --name-only --diff-filter=d)"
-fi
-
-if [[ -n "$raw_files_to_check" ]]; then
- echo "*** Checking for lint violations in changed files ***************"
- echo
-
- echo "$raw_files_to_check" | while IFS=$'\n' read -r line; do
- printf '%s\0' "$line"
- done | xargs -0 yarn prettier --check --ignore-unknown || exit $?
-fi
+scripts/lint-changed-files.sh --check
echo
echo "*** Auditing dependencies ***************"
diff --git a/.nvmrc b/.nvmrc
index e6db45a9..b009dfb9 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-18.14.0
+lts/*
diff --git a/.prettierignore b/.prettierignore
index 8fe001fd..e45057a8 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,5 +1,8 @@
.yarn/cache
.yarn/releases
gemfiles
+site
+tea.yaml
+tmp
vendor/bundle
yarn.lock
diff --git a/.python-version b/.python-version
new file mode 100644
index 00000000..8531a3b7
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12.2
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
deleted file mode 100644
index 3440b775..00000000
--- a/ARCHITECTURE.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Architecture
-
-I'll have more later around this,
-but here are some quick hits:
-
-## Basic concepts
-
-- An **object inspector** generates a multi-line textual representation of an object,
- similar to PrettyPrinter in Ruby or AwesomePrint,
- but more appropriate for showing within a diff.
-- An **operation** represents a difference in one value from another
- in the context of a data structure.
- That difference can either be an _delete_, _insert_, or _change_.
-- An **operation tree** is the set of all operations between two data structures,
- where each operation represents the difference between an inner element within the structure
- (value for an array or a key/value pair for a hash).
- Since change operations represent elements that have child elements,
- they also have child operations to represent those child elements.
- Those child operations can themselves have children, etc.
- This descendancy is what forms the tree.
-- An **operation tree builder** makes a comparison between two like data structures
- and generates an operation tree to represent the differences.
-- A **diff formatter** takes an operation tree
- and spits out a textual representation of that tree in the form of a conventional diff.
- Each operation may in fact generate more than one line in the final diff
- because the object that is specific to the operation is run through an object inspector.
-- Finally, a **differ** ties everything together
- and figures out which operation tree builder and diff formatter to use for a particular pair of values
- (where one value is the "expected" and the other is the "actual").
-
-## Code flow diagram
-
-[](https://docs.google.com/drawings/d/1nKi4YKXgzzIIM-eY0P4uwjkglmuwlf8nTRFne8QZhBg/edit)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 570534ed..951aa85e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,174 @@
# Changelog
+## 0.12.1 - 2024-04-26
+
+Note that since 0.12.0 has been yanked, changes for this version are listed
+alongside changes for 0.12.1. Also, changelog entries that were mistakenly
+omitted for 0.12.0 are included below as well.
+
+### Features
+
+- Create a proper space for docs, add info on architecture, and deploy docs
+ to a docsite automatically.
+ ([#224](https://github.com/mcmire/super_diff/pull/224),
+ [#225](https://github.com/mcmire/super_diff/pull/225),
+ [#226](https://github.com/mcmire/super_diff/pull/226),
+ [#232](https://github.com/mcmire/super_diff/pull/232),
+ [#233](https://github.com/mcmire/super_diff/pull/233),
+ [#245](https://github.com/mcmire/super_diff/pull/245))
+ - The `docs/` directory now holds information on contributing, which was
+ previously located at `CONTRIBUTING.md`, as well as information on using the
+ gem, which was previously located in `README.md`.
+ - However, crucially, `docs/` also now includes a breakdown of how this
+ project is structured and how the diffing engine works. This is hopefully
+ helpful to people who want to submit changes to this project.
+ - Additionally, starting with this release, the Markdown files in `docs/` will
+ published to a docsite, which can be viewed at
+ .
+ - Publishing of the docsite is automated: when a new release is issued, a new
+ version of the docsite will be published for that release under
+ .
+ ( will always redirect to the latest
+ release.)
+ - If any file in `docs/` is modified in a pull request, a new version of the
+ docsite will also be automatically deployed just for that pull request,
+ located under
+ .
+- Support the use of primary keys other than `id` when diffing ActiveRecord
+ models. ([#237](https://github.com/mcmire/super_diff/pull/237))
+
+### Bug fixes
+
+- Remove rogue `pp` statement
+ ([#242](https://github.com/mcmire/super_diff/pull/242))
+
+### Other notable changes
+
+- Reorganize codebase ([#230](https://github.com/mcmire/super_diff/pull/230))
+ - To be able to explain the architecture of this project more easily,
+ differs, inspection tree builders, operation tree builders, operation tree
+ flatteners, and operation trees for Ruby have now been relocated under a
+ `Basic` feature module, located in `lib/super_diff/basic`, which mirrors
+ `lib/super_diff/active_record`, `lib/super_diff/active_support`, and
+ `lib/super_diff/rspec`.
+ - Additionally, all of the files that were previously in `lib/super_diff` have
+ been moved to a `Core` module, and to make the file structure a little
+ flatter, `InspectionTreeBuilders` in various feature modules have been
+ removed from the `ObjectInspection` namespace.
+ - To maintain backward compatibility, all of the original constants still
+ exist, but they've been deprecated, and attempting to use them will result
+ in a warning. They will be removed in a future version.
+ - For full transparency, here is the list of renames:
+ - The following constants that were previously available under `SuperDiff`
+ are now located under `SuperDiff::Core`:
+ - `ColorizedDocumentExtensions`
+ - `Configuration`
+ - `GemVersion`
+ - `Helpers`
+ - `ImplementationChecks`
+ - `Line`
+ - `RecursionGuard`
+ - `TieredLines`
+ - `TieredLinesElider`
+ - `TieredLinesFormatter`
+ - Everything under `SuperDiff::Differs` is now under
+ `SuperDiff::Basic::Differs`
+ - All error classes under `SuperDiff::Errors` have been moved out and are
+ now directly under `SuperDiff::Core`
+ - `SuperDiff::ObjectInspection::InspectionTree` is now
+ `SuperDiff::Core::InspectionTree`
+ - Everything under `SuperDiff::ObjectInspection::InspectionTreeBuilders` is
+ now under `SuperDiff::Core::InspectionTreeBuilders`
+ - Everything under `SuperDiff::ObjectInspection::Nodes` is now under
+ `SuperDiff::Core::InspectionTreeNodes`
+ - Everything under `SuperDiff::OperationTreeBuilders` is now under
+ `SuperDiff::Basic::OperationTreeBuilders`
+ - Everything under `SuperDiff::OperationTreeFlatteners` is now under
+ `SuperDiff::Basic::OperationTreeFlatteners`
+ - Everything under `SuperDiff::OperationTrees` is now under
+ `SuperDiff::Basic::OperationTrees`
+ - Everything under `SuperDiff::Operations` has been moved out and is now
+ directly under `SuperDiff::Core`
+ - Everything under
+ - `SuperDiff::ActiveRecord::ObjectInspection::InspectionTreeBuilders` is now
+ under `SuperDiff::ActiveRecord::InspectionTreeBuilders`
+ - Everything under
+ - `SuperDiff::ActiveSupport::ObjectInspection::InspectionTreeBuilders` is
+ now under `SuperDiff::ActiveSupport::InspectionTreeBuilders`
+ - Everything under
+ - `SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders` is now under
+ `SuperDiff::RSpec::InspectionTreeBuilders`
+
+### Contributors
+
+This release features the following contributors:
+
+- [@benk-gc](https://github.com/benk-gc)
+- [@sidane](https://github.com/sidane)
+
+Thank you!
+
+## 0.12.0 - 2024-04-24 [YANKED]
+
+> [!WARNING]
+> This release has been yanked, as it included changes that weren't properly
+> logged in the changelog. This release wasn't ideal as it contained some
+> leftover print statements, anyway.
+
+### Features
+
+- Support the use of primary keys other than `id` when diffing ActiveRecord
+ models. ([#237](https://github.com/mcmire/super_diff/pull/237))
+
+### Contributors
+
+This release features the following contributors:
+
+- [@benk-gc](https://github.com/benk-gc)
+
+Thank you!
+
## 0.11.0 - 2024-02-10
+### BREAKING CHANGES
+
+- Change InspectionTree so that it no longer `instance_eval`s the block it
+ takes. ([#210](https://github.com/mcmire/super_diff/issues/210))
+ - If you have a custom InspectionTreeBuilder, you will need to change your
+ `call` method so that instead of looking like this:
+ ```ruby
+ def call
+ SuperDiff::ObjectInspection::InspectionTree.new do
+ as_lines_when_rendering_to_lines(collection_bookend: :open) do
+ add_text object.inspect
+ end
+ end
+ end
+ ```
+ it looks something like this instead:
+ ```ruby
+ def call
+ SuperDiff::ObjectInspection::InspectionTree.new do |t1|
+ t1.as_lines_when_rendering_to_lines(collection_bookend: :open) do |t2|
+ t2.add_text object.inspect
+ end
+ end
+ end
+ ```
+ Note that the following methods yield a new InspectionTree, so the tree
+ needs to be given a new name each time. It is conventional to use `t1`,
+ `t2`, etc.:
+ - `as_lines_when_rendering_to_lines`
+ - `as_prefix_when_rendering_to_lines`
+ - `as_prelude_when_rendering_to_lines`
+ - `as_single_line`
+ - `nested`
+ - `only_when`
+ - `when_empty`
+ - `when_non_empty`
+ - `when_rendering_to_lines`
+ - `when_rendering_to_string`
+
### Features
- Add inspector for RSpec describable matchers not otherwise handled by an
@@ -25,8 +192,6 @@
### Improvements
-- Change InspectionTree so that it no longer `instance_eval`s the block it
- takes. ([#210](https://github.com/mcmire/super_diff/issues/210))
- Improve wording in `raise_error` failure messages.
([#218](https://github.com/mcmire/super_diff/issues/218))
@@ -122,8 +287,6 @@ Thank you!
([#91])
-### Features
-
- Update inspection of Doubles to include stubbed methods and their values.
([#91])
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 20094255..00000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Contributing
-
-Want to make a change to this library?
-Great! Here's how you do that.
-
-First, create a fork of this repo,
-cloning it to your computer
-and running the following command in the resulting directory
-in order to install dependencies:
-
-```
-bin/setup
-```
-
-After this, you can run all of the tests
-to make sure everything is kosher:
-
-```
-bundle exec rake
-```
-
-Next, make changes to the code as necessary.
-
-Code is linted and formatted using Prettier,
-so [make sure that's set up in your editor first][prettier-editors],
-or you can always fix any lint violations by running:
-
-```
-yarn lint:fix
-```
-
-[prettier-editors]: https://prettier.io/docs/en/editors.html
-
-If you update one of the tests,
-you can run it like so:
-
-```
-bin/rspec spec/integration/...
-bin/rspec spec/unit/...
-```
-
-Finally, submit your PR.
-I'll try to respond as quickly as I can.
-I may have suggestions about code style or your approach,
-but hopefully everything looks good and your changes get merged!
-Now you're a contributor! đ
-
-## Speeding up the integration tests
-
-The integration tests,
-located in `spec/integration`,
-can be quite slow to run.
-If you'd like to speed them up,
-run the following command in a separate tab:
-
-```
-zeus start
-```
-
-Now the next time you run an integration test by saying
-
-```
-bin/rspec spec/integration/...
-```
-
-it should run twice as fast.
-
-## Understanding the codebase
-
-If you want to make a change
-but you're having trouble where to start,
-you might find the [Architecture](./ARCHITECTURE.md) document helpful.
diff --git a/README.md b/README.md
index 7bed3d5e..8c2843eb 100644
--- a/README.md
+++ b/README.md
@@ -8,24 +8,23 @@
[issuehunt-badge]: https://img.shields.io/badge/sponsored_through-IssueHunt-2EC28C
[issuehunt]: https://issuehunt.io/r/mcmire/super_diff
-SuperDiff is a gem that hooks into RSpec
-to intelligently display the differences between two data structures of any type.
+**SuperDiff** is a Ruby gem
+which is designed to display the differences between two objects of any type
+in a familiar and intelligent fashion.
đĸ **[See what's changed in recent versions.][changelog]**
-[changelog]: CHANGELOG.md
+[changelog]: ./CHANGELOG.md
## Introduction
The primary motivation behind this gem
is to vastly improve upon RSpec's built-in diffing capabilities.
-
-Sometimes, whenever you use a matcher such as `eq`, `match`, `include`, or `have_attributes`,
+RSpec has many nice features,
+and one of them is that whenever you use a matcher such as `eq`, `match`, `include`, or `have_attributes`,
you will get a diff of the two data structures you are trying to match against.
This is great if all you want to do is compare multi-line strings.
-But if you want to compare other, more "real world" kinds of values,
-such as what you might work with when developing API endpoints
-or testing methods that make database calls and return a set of model objects,
+But if you want to compare other, more "real world" kinds of values such as API or database data,
then you are out of luck.
Since [RSpec merely runs your `expected` and `actual` values through Ruby's PrettyPrinter library][rspec-differ-fail]
and then performs a diff of these strings,
@@ -33,8 +32,7 @@ the output it produces leaves much to be desired.
[rspec-differ-fail]: https://github.com/rspec/rspec-support/blob/c69a231d7369dd165ad7ce4742e1a2e21e3462b5/lib/rspec/support/differ.rb#L178
-For instance,
-let's say you wanted to compare these two hashes:
+For instance, let's say you wanted to compare these two hashes:
```ruby
actual = {
@@ -78,7 +76,7 @@ expect(actual).to eq(expected)
You would get output that looks like this:
-
+
What this library does
is to provide a diff engine
@@ -87,162 +85,14 @@ and display them in a sensible way.
So, using the example above,
you'd get this instead:
-
-
-## Installation
-
-There are a few different ways to install `super_diff`
-depending on your type of project.
-
-### Rails apps
-
-If you're developing a Rails app,
-add the following to your Gemfile:
-
-```ruby
-group :test do
- gem "super_diff"
-end
-```
-
-After running `bundle install`,
-add the following to your `rails_helper`:
-
-```ruby
-require "super_diff/rspec-rails"
-```
-
-### Projects using some part of Rails (e.g. ActiveModel)
-
-If you're developing an app using Hanami or Sinatra,
-or merely using a part of Rails such as ActiveModel,
-add the following to your Gemfile where appropriate:
-
-```ruby
-gem "super_diff"
-```
-
-After running `bundle install`,
-add the following to your `spec_helper`:
-
-```ruby
-require "super_diff/rspec"
-require "super_diff/active_support"
-```
-
-### Gems
-
-If you're developing a gem,
-add the following to your gemspec:
-
-```ruby
-spec.add_development_dependency "super_diff"
-```
-
-Now add the following to your `spec_helper`:
-
-```ruby
-require "super_diff/rspec"
-```
-
-## Configuration
-
-You can customize the behavior of the gem
-by adding a configuration block
-to your test helper file
-(`rails_helper` or `spec_helper`)
-which looks something like this:
-
-```ruby
-SuperDiff.configure do |config|
- # ...
-end
-```
-
-### Customizing colors
-
-If you don't like the colors that SuperDiff uses,
-you can change them like this:
+
-```ruby
-SuperDiff.configure do |config|
- config.actual_color = :green
- config.expected_color = :red
- config.border_color = :yellow
- config.header_color = :yellow
-end
-```
-
-See [eight_bit_color.rb](lib/super_diff/csi/eight_bit_color.rb)
-for the list of available colors.
-
-You can also completely disable colorized output.
-
-
-```ruby
-SuperDiff.configure do |config|
- config.color_enabled = false
- end
-```
-
+## Installation & Usage
-### Disabling the key
+đ For more on how to install and use SuperDiff,
+[read the user documentation][user-docs].
-You can disable the key by changing the following config (default: true):
-
-
-```ruby
-SuperDiff.configure do |config|
- config.key_enabled = false
-end
-```
-
-
-### Hiding unimportant lines
-
-When looking at a large diff for which many of the lines do not change,
-it can be difficult to locate the lines which do. Text-oriented
-diffs such as those you get from a conventional version control system
-solve this problem by removing those unchanged lines from the diff
-entirely. The same can be done in SuperDiff.
-
-```ruby
-SuperDiff.configure do |config|
- config.diff_elision_enabled = false
- config.diff_elision_maximum = 3
-end
-```
-
-- `diff_elision_enabled` â The elision logic is disabled by default so
- as not to surprise people, so setting this to `true` will turn it on.
-- `diff_elision_maximum` â This number controls what happens to
- unchanged lines (i.e. lines that are neither "insert" lines nor
- "delete" lines) that are in between changed lines. If a section of
- unchanged lines is beyond this number, the gem will elide (a fancy
- word for remove) the data structures within that section as much as
- possible until the limit is reached or it cannot go further. Elided
- lines are replaced with a `# ...` marker.
-
-### Diffing custom objects
-
-If you are comparing two data structures
-that involve a class that is specific to your project,
-the resulting diff may not look as good as diffs involving native or primitive objects.
-This happens because if SuperDiff doesn't recognize a class,
-it will fall back to a generic representation when diffing instances of that class.
-Fortunately, the gem has a pluggable interface
-that allows you to insert your own implementations
-of key pieces involved in the diffing process.
-I'll have more about how that works soon,
-but here is what such a configuration would look like:
-
-```ruby
-SuperDiff.configure do |config|
- config.add_extra_differ_class(YourDiffer)
- config.add_extra_operation_tree_builder_class(YourOperationTreeBuilder)
- config.add_extra_operation_tree_class(YourOperationTree)
-end
-```
+[user-docs]: ./docs/users/getting-started.md
## Support
@@ -257,16 +107,19 @@ I'll try to respond to it as soon as I can!
## Contributing
Any code contributions to improve this library are welcome!
-Please see the [contributing](./CONTRIBUTING.md) document for more on how to do that.
+Please see the [contributing](./docs/contributors/index.md) document
+for more on how to do that.
## Sponsoring
If there's a change you want implemented, you can choose to sponsor that change!
`super_diff` is set up on IssueHunt,
so feel free to search for an existing issue (or make your own)
-and [add a bounty](https://issuehunt.io/r/mcmire/super_diff).
+and [add a bounty][issuehunt].
I'll get notified right away!
+[issuehunt]: https://issuehunt.io/r/mcmire/super_diff
+
## Compatibility
`super_diff` is [tested][gh-actions] to work with
diff --git a/bin/setup b/bin/setup
index 8533cb28..6c9551fa 100755
--- a/bin/setup
+++ b/bin/setup
@@ -19,7 +19,7 @@ provision-project() {
#
# To regenerate this section, install the gem and run:
#
-# generate-setup -p ruby -p node
+# generate-setup -p ruby -p node -p python
#
# --- SETUP --------------------------------------------------------------------
@@ -332,9 +332,12 @@ ensure-project-node-dependencies-installed() {
banner 'Installing Node dependencies'
npm install
elif [[ -f yarn.lock ]]; then
- if ! type yarn &>/dev/null || ! yarn --version &>/dev/null; then
- banner 'Installing Yarn'
- npm install -g yarn
+ if ! has-executable yarn || ! yarn --version &>/dev/null; then
+ banner 'Enabling Yarn'
+ corepack enable
+ if has-executable asdf; then
+ asdf reshim nodejs
+ fi
fi
banner 'Installing Node dependencies'
yarn install
@@ -344,15 +347,88 @@ ensure-project-node-dependencies-installed() {
It doesn't look like you have a package-lock.json or yarn.lock in your project
yet. I'm not sure which package manager you plan on using, so you'll need to run
either \`npm install\` or \`yarn install\` once first. Additionally, if you want
-to use Yarn 2+, then now is the time to switch to that. Then you can re-run this
+to use Yarn 2, then now is the time to switch to that. Then you can re-run this
script."
exit 1
fi
}
+# --- PYTHON -------------------------------------------------------------------
+
+REQUIRED_PYTHON_VERSION=
+
+provision-python() {
+ if [[ -f .tool-versions ]]; then
+ REQUIRED_PYTHON_VERSION=$(cat .tool-versions | grep '^python ' | head -n 1 | sed -Ee 's/^python (.+)$/\1/')
+ elif [[ -f .python-version ]]; then
+ REQUIRED_PYTHON_VERSION=$(cat .python-version | head -n 1 | sed -Ee 's/^python-([[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+)$/\1/')
+ fi
+
+ if [[ -z $REQUIRED_PYTHON_VERSION ]]; then
+ error "Could not determine required Python version for this project."
+ print-wrapped "\
+Your project needs to include either a valid .tool-versions file with a 'python'
+line or a valid .python-version file."
+ exit 1
+ fi
+
+ ensure-python-installed
+ ensure-pipx-installed
+
+ if [[ -f pyproject.toml ]] || [[ -f requirements.txt ]]; then
+ ensure-project-python-dependencies-installed
+ fi
+}
+
+ensure-python-installed() {
+ if has-executable asdf; then
+ if ! (asdf current python | grep $REQUIRED_PYTHON_VERSION'\>' &>/dev/null); then
+ banner "Installing Python $REQUIRED_PYTHON_VERSION with asdf"
+ asdf install python $REQUIRED_PYTHON_VERSION
+ fi
+ elif has-executable pyenv; then
+ if ! (pyenv versions | grep $REQUIRED_PYTHON_VERSION'\>' &>/dev/null); then
+ banner "Installing Python $REQUIRED_PYTHON_VERSION with pyenv"
+ pyenv install --skip-existing "$REQUIRED_PYTHON_VERSION"
+ fi
+ else
+ error "You don't seem to have a Python manager installed."
+ print-wrapped "\
+We recommend using asdf. You can find instructions to install it here:
+
+ https://asdf-vm.com
+
+When you're done, close and re-open this terminal tab and re-run this script."
+ exit 1
+ fi
+}
+
+ensure-pipx-installed() {
+ if ! has-executable pipx; then
+ banner "Installing pipx"
+ pip install --user pipx
+ fi
+}
+
+ensure-project-python-dependencies-installed() {
+ banner 'Installing Python dependencies'
+
+ if [[ -f pyproject.toml ]]; then
+ if ! has-executable poetry; then
+ banner "Installing Poetry"
+ pipx install poetry
+ fi
+
+ poetry install
+ else
+ warning "Did not detect a way to install Python dependencies."
+ fi
+}
+
run-provisions() {
provision-ruby
provision-node
+ provision-python
}
# --- FIN ----------------------------------------------------------------------
diff --git a/docs/after.rb b/docs-support/after.rb
similarity index 100%
rename from docs/after.rb
rename to docs-support/after.rb
diff --git a/docs/before.rb b/docs-support/before.rb
similarity index 100%
rename from docs/before.rb
rename to docs-support/before.rb
diff --git a/docs/carbon-config.json b/docs-support/carbon-config.json
similarity index 100%
rename from docs/carbon-config.json
rename to docs-support/carbon-config.json
diff --git a/docs/carbon.md b/docs-support/carbon.md
similarity index 100%
rename from docs/carbon.md
rename to docs-support/carbon.md
diff --git a/docs/code-flow-diagram.png b/docs-support/code-flow-diagram.png
similarity index 100%
rename from docs/code-flow-diagram.png
rename to docs-support/code-flow-diagram.png
diff --git a/docs/after.png b/docs/assets/after.png
similarity index 100%
rename from docs/after.png
rename to docs/assets/after.png
diff --git a/docs/before.png b/docs/assets/before.png
similarity index 100%
rename from docs/before.png
rename to docs/assets/before.png
diff --git a/docs/contributors/architecture/how-rspec-works.md b/docs/contributors/architecture/how-rspec-works.md
new file mode 100644
index 00000000..6c79e381
--- /dev/null
+++ b/docs/contributors/architecture/how-rspec-works.md
@@ -0,0 +1,228 @@
+# How RSpec works
+
+In order to understand how the RSpec integration in SuperDiff works,
+it's important to study the pieces in play within RSpec itself.
+
+## Context
+
+Imagine a file such as the following:
+
+```ruby
+# spec/some_spec.rb
+describe "Some tests" do
+ it "does something" do
+ expect([1, 2, 3]).to eq([1, 6, 3])
+ end
+end
+```
+
+Then, imagine that the user runs:
+
+```
+rspec
+```
+
+Without SuperDiff activated,
+this will produce the following output:
+
+```
+Some tests
+ does something (FAILED - 1)
+
+Failures:
+
+ 1) Some tests does something
+ Failure/Error: expect([1, 2, 3]).to eq([1, 6, 3])
+
+ expected: [1, 6, 3]
+ got: [1, 2, 3]
+
+ (compared using ==)
+ # ./spec/some_spec.rb:3:in `block (2 levels) in '
+
+Finished in 0.01186 seconds (files took 0.07765 seconds to load)
+1 example, 1 failure
+
+Failed examples:
+
+rspec ./spec/some_spec.rb:2 # Some tests does something
+```
+
+Now imagine that we want to modify this output
+to replace the "expected:"/"actual:" lines with a diff.
+How would we do this?
+
+## RSpec's cast of characters
+
+First, we will review several concepts in RSpec: [^fn1]
+
+- Since RSpec tests are "just Ruby",
+ parts of tests map to objects
+ which are created when those tests are loaded.
+ `describe`s and `context`s are represented by
+ **example groups**,
+ instances of [`RSpec::Core::ExampleGroup`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example_group.rb),
+ and `it`s and `specify`s are represented by
+ **examples**,
+ instances of [`RSpec::Core::Example`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example.rb).
+- Most notably,
+ within tests themselves,
+ the `expect` method â
+ [mixed into tests via the syntax layer][rspec-exp-syntax] â
+ returns an instance of [`RSpec::Expectations::ExpectationTarget`](https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/expectation_target.rb),
+ and may raise an error if the check it is performing fails.
+- **Configuration** is kept in an instance of [`RSpec::Core::Configuration`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/configuration.rb),
+ which is accessible via `RSpec.configuration`
+ and is [initialized the first time it's used][rspec-configuration-init]
+- The **runner**,
+ an instance of [`RSpec::Core::Runner`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/runner.rb),
+ is the entrypoint to all of RSpec â
+ [it's called directly by the `rspec` executable][rspec-core-runner-call] â
+ and executes the tests the user has specified.
+- **Formatters** change RSpec's output after running tests.
+ Since the user can specify one formatter when running `rspec`,
+ the collection of registered formatters is managed by the formatter loader,
+ an instance of [`RSpec::Core::Formatters::Loader`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters.rb#L96).
+ The default formatter is "progress",
+ [set in the configuration object][rspec-default-formatter-set],
+ which maps to an instance of [`RSpec::Core::Formatters::ProgressFormatter`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters/progress_formatter.rb).
+- **[Notifications](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb)**
+ represent events that occur while running tests,
+ such as "these tests failed"
+ or "this test was skipped".
+- The **reporter**,
+ an instance of [`RSpec::Core::Reporter`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb),
+ acts as sort of the brain of the whole operation.
+ Implementing a publish/subscribe model,
+ it tracks the state of tests as they are run,
+ including errors captured during the process,
+ packaging key moments into notifications
+ and delegating them to all registered formatters (or anything else listening to the reporter).
+ Like the configuration object,
+ it is also global,
+ accessible via the configuration object,
+ and is [initialized the first time it's used][rspec-reporter-init]
+- The **exception presenter**,
+ an instance of [`RSpec::Core::Formatters::ExceptionPresenter`](https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters/exception_presenter.rb),
+ is a special type of formatter
+ which does not respond to events,
+ but is rather responsible for managing all of the logic involved
+ in building all of the output that appears
+ when a test fails.
+
+## What RSpec does
+
+Given the above, RSpec performs the following sequence of events:
+
+
+
+1. The developer adds an failing assertion to a test using the following forms
+ (filling in ``, ``, ``, and `` appropriately):
+ - `expect().to ()`
+ - `expect { }.to ()`
+ - `expect().not_to ()`
+ - `expect { }.not_to ()`
+1. The developer runs the test using the `rspec` executable.
+1. The `rspec` executable [calls `RSpec::Core::Runner.invoke`][rspec-core-runner-call].
+1. Skipping a few steps, `RSpec::Core::Runner#run_specs` is called,
+ which [runs all tests by surrounding them in a call to `RSpec::Core::Reporter#report`][rspec-reporter-report-call].
+1. Skipping a few more steps, [`RSpec::Core::Example#run` is called to run the current example][rspec-core-example-run-call].
+1. From here one of two paths is followed
+ depending on whether the assertion is positive (`.to`) or negative (`.not_to`).
+ - If the assertion is positive:
+ 1. Within the test,
+ after `expect` is called to build a `RSpec::Expectations::ExpectationTarget`,
+ [the `to` method calls `RSpec::Expectations::PositiveExpectationHandler.handle_matcher`][rspec-positive-expectation-handler-handle-matcher-call].
+ 1. The matcher is then used to know
+ whether the assertion passes or fails:
+ `PositiveExpectationHandler`
+ [calls the `matches?` method on the matcher][rspec-positive-expectation-handler-matcher-matches].
+ 1. Assuming that `matches?` returns false,
+ `PositiveExpectationHandler` then [calls `RSpec::Expectations::ExpectationHelper.handle_failure`][rspec-expectation-helper-handle-failure-call-positive],
+ telling it to get the positive failure message from the matcher
+ by calling `failure_message`.
+ - If the assertion is negative:
+ 1. Within the test,
+ after `expect` is called to build a `RSpec::Expectations::ExpectationTarget`,
+ [the `not_to` method calls `RSpec::Expectations::NegativeExpectationHandler.handle_matcher`][rspec-negative-expectation-handler-handle-matcher-call].
+ 1. The matcher is then used to know
+ whether the assertion passes or fails:
+ `NegativeExpectationHandler`,
+ [calls the `does_not_match?` method on the matcher][rspec-negative-expectation-handler-matcher-does-not-match].
+ 1. Assuming that `does_not_match?` returns false,
+ `NegativeExpectationHandler` then [calls `RSpec::Expectations::ExpectationHelper.handle_failure`][via `NegativeExpectationHandler`][rspec-expectation-helper-handle-failure-call-negative],
+ telling it to get the negative failure message from the matcher
+ by calling `failure_message_when_negated`.
+1. `RSpec::Expectations::ExpectationHelper.handle_failure` [calls `RSpec::Expectations.fail_with`][rspec-expectations-fail-with-call].
+1. `RSpec::Expectations.fail_with` [creates a diff using `RSpec::Matchers::MultiMatcherDiff`,
+ wraps it in an exception,
+ and feeds the exception to `RSpec::Support.notify_failure`][rspec-support-notify-failure-call].
+1. `RSpec::Support.notify_failure` calls the currently set failure notifier,
+ which by default [raises the given exception][rspec-support-exception-raise].
+1. Returning to `RSpec::Core::Example#run`,
+ this method [rescues the exception][rspec-core-example-run-rescue]
+ and then calls `finish`,
+ which [calls `example_failed` on the reporter][rspec-reporter-example-failed-call].
+1. `RSpec::Core::Reporter#example_failed` uses `RSpec::Core::Notifications::ExampleNotification.for`
+ to [construct a notification][rspec-reporter-construct-failed-example-notification],
+ which in this case is an `RSpec::Core::Notifications::FailedExampleNotification`.
+ `RSpec::Core::Notifications::FailedExampleNotification` in turn
+ [constructs an `RSpec::Core::Formatters::ExceptionPresenter`][rspec-exception-presenter-init].
+1. `RSpec::Core::Reporter#example_failed` then [passes the notification object
+ along with an event of `:example_failed` to the `notify` method][rspec-reporter-example-failed-call].
+ Because `RSpec::Core::Formatters::ProgressFormatter` is a listener on the reporter,
+ [its `example_failed` method gets called][rspec-progress-formatter-example-failed-call],
+ which prints a message `Failure:` to the terminal.
+1. Returning to `RSpec::Core::Reporter#report`,
+ it now [calls `finish` after all tests are run][rspec-reporter-finish-call].
+1. `RSpec::Core::Reporter#finish` [notifies listeners of the `:dump_failures` event][rspec-reporter-notify-dump-failures],
+ this time using an instance of `RSpec::Core::Notifications::ExamplesNotification`.
+ Again, because `RSpec::Core::Formatters::ProgressFormatter` is registered,
+ its `dump_failures` method is called,
+ which is actually defined in `RSpec::Core::Formatters::BaseTextFormatter`.
+1. `RSpec::Core::Formatters::BaseTextFormatter#dump_failures`
+ [calls `RSpec::Core::Notifications::ExamplesNotification#fully_formatted_failed_examples`][rspec-examples-notification-fully-formatted-failed-examples-call].
+1. `RSpec::Core::Notifications::ExamplesNotification#fully_formatted_failed_examples`
+ [formats all of the failed examples][rspec-failed-examples-notification-fully-formatted-call]
+ by wrapping them in `RSpec::Core::Notifications::FailedExampleNotification`s and calling `fully_formatted` on them.
+1. `RSpec::Core::Notifications::FailedExampleNotification#fully_formatted` then [calls `fully_formatted`
+ on its `RSpec::Core::Formatters::ExceptionPresenter`][rspec-exception-presenter-fully-formatted-call].
+1. `RSpec::Core::Formatters::ExceptionPresenter#fully_formatted` then [constructs various pieces
+ of what will eventually be printed to the terminal][rspec-exception-presenter-main],
+ including the name of the test,
+ the line that failed,
+ the error and backtrace,
+ and other pertinent details.
+
+
+
+[^fn1]: Note that the analysis of the RSpec source code in this document is accurate as of RSpec v3.13.0, released February 4, 2024.
+
+[rspec-exp-syntax]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/syntax.rb#L73
+[rspec-configuration-init]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core.rb#L86
+[rspec-core-runner-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/exe/rspec#L4
+[rspec-default-formatter-set]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/configuration.rb#L1030
+[rspec-reporter-init]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/configuration.rb#L1056
+[rspec-reporter-report-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/runner.rb#L115
+[rspec-core-example-run-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example_group.rb#L646
+[rspec-positive-expectation-handler-handle-matcher-call]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/expectation_target.rb#L65
+[rspec-negative-expectation-handler-handle-matcher-call]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/expectation_target.rb#L78
+[rspec-positive-expectation-handler-matcher-matches]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L51
+[rspec-negative-expectation-handler-matcher-does-not-match]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L79
+[rspec-expectation-helper-handle-failure-call-positive]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L56
+[rspec-expectation-helper-handle-failure-call-negative]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L84
+[rspec-expectations-fail-with-call]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/handler.rb#L37-L41
+[rspec-support-notify-failure-call]: https://github.com/rspec/rspec-expectations/blob/v3.13.0/lib/rspec/expectations/fail_with.rb#L27-L35
+[rspec-support-exception-raise]: https://github.com/rspec/rspec-support/blob/v3.13.0/lib/rspec/support.rb#L110
+[rspec-core-example-run-rescue]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example.rb#L280
+[rspec-reporter-example-failed-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/example.rb#L484
+[rspec-reporter-construct-failed-example-notification]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb#L52
+[rspec-exception-presenter-init]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb#L213
+[rspec-reporter-example-failed-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb#L145
+[rspec-progress-formatter-example-failed-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb#L209
+[rspec-reporter-finish-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb#L76
+[rspec-reporter-notify-dump-failures]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/reporter.rb#L178
+[rspec-examples-notification-fully-formatted-failed-examples-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters/base_text_formatter.rb#L32
+[rspec-failed-examples-notification-fully-formatted-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb#L114
+[rspec-exception-presenter-fully-formatted-call]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/notifications.rb#L202
+[rspec-exception-presenter-main]: https://github.com/rspec/rspec-core/blob/v3.13.0/lib/rspec/core/formatters/exception_presenter.rb#L84-L100
diff --git a/docs/contributors/architecture/how-super-diff-works.md b/docs/contributors/architecture/how-super-diff-works.md
new file mode 100644
index 00000000..2b6935df
--- /dev/null
+++ b/docs/contributors/architecture/how-super-diff-works.md
@@ -0,0 +1,186 @@
+# How SuperDiff works
+
+## SuperDiff's cast of characters
+
+- An **inspection tree builder**
+ makes use of an **inspection tree**
+ to generate a multi-line textual representation of an object,
+ similar to PrettyPrinter in the Ruby standard library or the AwesomePrint gem,
+ but more appropriate for showing within a diff.
+- An **operation tree builder** makes a comparison between two objects
+ (the "expected" vs. the "actual")
+ and generates an operation tree to represent the differences.
+- An **operation tree** is made up of **operations**,
+ which designate differences in the inner parts of the two objects.
+ Those differences can be of type _delete_, _insert_, _change_, or _noop_.
+ Since objects can be nested,
+ some operations can have children operations themselves,
+ hence the tree.
+- An **operation tree flattener** takes an operation tree
+ and converts them to a set of **lines**,
+ which will aid in generating a diff.
+ Logic is applied to determine whether to place prefixes, suffixes, or commas.
+ Each operation may in fact generate more than one line
+ because the object that is specific to the operation is run through an inspector.
+- A **diff formatter** takes a set of lines
+ and spits out a textual representation in the form of a conventional diff.
+- A **differ** ties everything together
+ by figuring out which operation tree builder to use for a pair of expected and actual values,
+ building an operation tree,
+ and then converting it to a diff.
+
+## Where SuperDiff integrates into RSpec
+
+As described in ["How RSpec works"](./how-rspec-works.md#what-rspec-does),
+when an assertion in a test fails â
+which happens when a matcher whose `#matches?` method returns `false`
+is passed to `expect(...).to`,
+or when a matcher whose `#does_not_match?` method returns `true`
+is passed to `expect(...).not_to` â
+RSpec will call the `RSpec::Expectations::ExpectationHelper#handle_failure` method,
+which will call `RSpec::Expectations.fail_with`.
+This method will use `RSpec::Matchers::ExpectedsForMultipleDiffs`
+and the differ object that `RSpec::Expectations.differ` returns
+to generate a diff,
+combining it with the failure message from the matcher,
+obtained by either calling `failure_message` or `failure_messsage_when_negated`,
+and then it will bundle them both into an error object.
+RSpec's runner will eventually capture this error and hand it to the reporter,
+which will display it via `RSpec::Core::Formatters::ExceptionPresenter`.
+
+Given this, there are a few things that SuperDiff needs to do
+in order to integrate fully with RSpec.
+
+1. First,
+ SuperDiff needs to get RSpec to use its differ instead of its own.
+ Unfortunately, while RSpec is very configurable,
+ it does not allow its differ to be substituted,
+ so the gem needs to do some amount of patching in order to achieve this.
+2. Second,
+ the gem needs to provide intelligent diffing
+ for all kinds of built-in matchers.
+ Many matchers in RSpec are marked as non-diffable â
+ their `#diffable?` method returns `false` â
+ causing RSpec to not show a diff after the matcher's failure message
+ in the failure output.
+ The `contain_exactly` matcher is one such example.
+ SuperDiff turns this on â
+ but the only way to do this is via patching.
+3. Lastly,
+ SuperDiff also modifies the failure messages for RSpec's built-in matchers
+ so that key words belonging to the "expected" and "actual" values
+ get recolored.
+ Again, the only real way to do this is via patching.
+
+Here are all of the places that SuperDiff patches RSpec:
+
+- `RSpec::Expectations.differ`
+ (to use `SuperDiff::RSpec::Differ` instead of RSpec's own differ)
+- `RSpec::Expectations::ExpectationHelper#handle_failure`
+ (to consult the matcher for the "expected" and "actual" values,
+ under special methods `expected_for_diff` and `actual_for_diff`)
+- `RSpec::Core::Formatters::ConsoleCodes`
+ (to allow for using SuperDiff's colors
+ and to remove the fallback in the absence of a specified color)
+- `RSpec::Core::Formatters::ExceptionPresenter`
+ (to recolor failure output)
+- `RSpec::Core::SyntaxHighlighter`
+ (to turn off syntax highlighting for code,
+ as it interferes with the previous patches)
+- `RSpec::Support::ObjectFormatter`
+ (to use SuperDiff's object inspection logic)
+- `RSpec::Matchers::ExpectedsForMultipleDiffs`
+ (to add a key above the diff,
+ add spacing around the diff,
+ and colorize the word "Diff:")
+- `RSpec::Matchers::Builtin::*`
+ (to reword failure messages across various matchers)
+- `RSpec::Matchers`
+ (to reset the `an_array_matching` alias for `match_array`,
+ and to ensure that `match_array` preserves behavior,
+ as it is backed by MatchArray class specific to SuperDiff)
+
+## How SuperDiff's diff engine works
+
+With the internals of RSpec thoroughly explored,
+the internals of SuperDiff can finally be enumerated.
+
+Once a test fails
+and RSpec delegates to SuperDiff's differ,
+this sequence of events occurs:
+
+1. `SuperDiff.diff` is called with a pair of values: `expected` and `actual`.
+ This method delegates to `SuperDiff::Core::DifferDispatcher.call`,
+ which looks for a differ via `SuperDiff.configuration`
+ which is suitable for the pair.
+ It does this by calling `.applies_to?` on each one,
+ passing the `expected` and `actual`;
+ the first differ for whom this method returns `true` wins.
+ (This is a common pattern throughout the codebase.)
+ In most cases, if no differs are suitable,
+ then an error is raised,
+ although this is sometimes overridden.
+1. Once a differ is found,
+ its `.call` method is called.
+ Since all differs inherit from `SuperDiff::Core::AbstractDiffer`,
+ `.call` always builds an operation tree,
+ but the type of operation tree to build
+ â or, more specifically, the operation tree builder subclass â
+ is determined by the differ itself,
+ via the `#operation_tree_builder_class` method.
+ For instance,
+ `SuperDiff::Basic::Differs::Array` uses a `SuperDiff::Basic::OperationTreeBuilders::Array`,
+ `SuperDiff::Basic::Differs::Hash` uses a `SuperDiff::Basic::OperationTreeBuilders::Hash`,
+ etc.
+1. Once the differ has an operation tree builder,
+ the differ calls `.call` on it
+ to build an operation tree.
+ Different operation tree builders do different things
+ depending on the types of objects,
+ but the end goal is to iterate over both the expected and actual values in tandem,
+ find the differences between them,
+ and represent those differences as operations.
+ An operation may be one of four types:
+ `:insert`, `:delete`, `:change`, or `:noop`.
+ In the case of collections â
+ which covers most types of values â
+ the diff is performed recursively.
+ This means that just as collections can have multiple levels,
+ so too can operation trees.
+1. Once the differ has an operation tree,
+ it then calls `#to_diff` on it.
+ This method is defined in `SuperDiff::Core::AbstractOperationTree`,
+ and it starts by first flattening the tree.
+1. This means that we need an operation tree flattener class.
+ Like differs,
+ operation trees specify which operation tree flattener they want to use
+ via the `operation_tree_flattener_class` method.
+1. Once the operation tree has a flattener class,
+ it calls `.call` on the class
+ to flatten the tree.
+1. Different types of flatteners also do different things,
+ but most of them operate on collection-based operation trees.
+ Since operation trees can have multiple level,
+ the flattening must be performed recursively.
+ The end result is a list of Line objects.
+1. Once the operation tree has been flattened,
+ then if the user has configured the gem to do so,
+ a step is performed to look for unchanged lines
+ (that is, operations of type `:noop`)
+ and _elide_ them â
+ collapse them in such a way that the surrounding context is still visible.
+1. Once a set of elided lines is obtained,
+ the operation tree runs them through `SuperDiff::Core::TieredLinesFormatter`,
+ which will add the `-`s and `+`s along with splashes of color
+ to create the final format you see at the very end.
+
+In summary:
+
+```mermaid
+graph TB
+ DifferDispatcher -- Configured differs --> Differ;
+ Differ -- Operation tree builder --> OperationTree[Operation tree];
+ OperationTree -- Operation tree flattener --> Lines;
+ Lines -- Tiered lines elider --> ElidedLines[Elided lines];
+ ElidedLines -- Tiered lines formatter --> FinalDiff[Diff string];
+```
diff --git a/docs/contributors/architecture/introduction.md b/docs/contributors/architecture/introduction.md
new file mode 100644
index 00000000..4cd3d277
--- /dev/null
+++ b/docs/contributors/architecture/introduction.md
@@ -0,0 +1,12 @@
+# Architecture
+
+The SuperDiff codebase is sufficiently complex
+that diving into the source code for the first time may be daunting.
+
+This section aims to point contributors in the right direction.
+Since most of the code in this codebase services the RSpec integration,
+these guides heavily skew toward that topic,
+and you can start with a [guide to how RSpec works if you like](./how-rspec-works.md).
+But you can also find [details around the diff engine](./how-super-diff-works.md).
+Finally, if you're curious about the files in this project and how they're organized,
+feel free to consult the [structure](./structure.md) document.
diff --git a/docs/contributors/architecture/structure.md b/docs/contributors/architecture/structure.md
new file mode 100644
index 00000000..c14f259a
--- /dev/null
+++ b/docs/contributors/architecture/structure.md
@@ -0,0 +1,375 @@
+# Structure
+
+To help you in your contributing journey,
+this page offers insight into how the SuperDiff codebase is laid out.
+
+Note that this page does not mention every single file:
+some files which are unimportant,
+such as those which only serve to require other files,
+have been omitted.
+
+## Implementation files
+
+All implementation files in the project are located in `lib/`,
+and most are located in `lib/super_diff/`.
+
+In `lib/super_diff` there are 7 notable directories,
+which create 4 layers:
+
+```mermaid
+flowchart BT
+ subgraph rails
+ active_record
+ active_support
+ end
+
+ basic --> core
+ rspec --> basic
+ active_record --> basic
+ active_support --> basic
+ active_record --> core
+ active_support --> core
+ rspec --> core
+ equality_matchers --> core
+ equality_matchers --> basic
+ core --> csi
+ rspec --> csi
+```
+
+Following is a breakdown of each directory.
+
+### Top level (`lib/`)
+
+- **`super_diff.rb`:**
+ In addition to loading the whole library,
+ this file contains helpers which are so useful that they can be called anywhere.
+
+### `SuperDiff::Core` (`lib/super_diff/core/`)
+
+The Core module provides building blocks, utilities, and other primitives
+that are used throughout the library.
+
+This directory can be grouped into the following themes:
+
+#### Initialization
+
+- **`configuration.rb`:**
+ Stores settings for SuperDiff
+ which can be used to control the behavior of various features.
+
+#### Differ
+
+- **`abstract_differ.rb`:**
+ The superclass for all differ classes.
+- **`differ_dispatcher.rb`:**
+ Finds the differ class that best matches a combination of "expected" and "actual" values,
+ then uses it to run the diff between them.
+- **`no_differ_available_error.rb`:**
+ The error produced when the DifferDispatcher fails to find a matching class.
+
+#### Operation tree
+
+- **`abstract_operation_tree_builder.rb`:**
+ The superclass for all operation tree builder classes,
+ defined elsewhere in SuperDiff or by users.
+- **`abstract_operation_tree.rb`:**
+ The superclass for all operation tree classes,
+ defined elsewhere in SuperDiff or by users.
+- **`abstract_operation_tree_flattener.rb`:**
+ The superclass for all operation tree flattener classes,
+ defined elsewhere in SuperDiff or by users.
+- **`binary_operation.rb`:**
+ A node in an operation tree which represents a comparison between two values.
+- **`no_operation_tree_available_error.rb`:**
+ The error produced when the OperationTreeFinder fails to find a matching class.
+- **`no_operation_tree_builder_available_error.rb`:**
+ The error produced when the OperationTreeBuilderDispatcher fails to find a matching class.
+- **`operation_tree_builder_dispatcher.rb`:**
+ Finds the operation tree builder class
+ that best matches a combination of "expected" and "actual" values,
+ then uses it to build the operation tree representing the difference between them.
+- **`operation_tree_finder.rb`:**
+ Finds the operation tree matching a value.
+- **`unary_operation.rb`:**
+ A node in an operation tree which represents an operation to a value.
+
+#### Lines
+
+- **`line.rb`:**
+ A deconstructed, mutable version of a line in the final diff output.
+- **`tiered_lines_elider.rb`:**
+ Collapses unchanged sections of a diff,
+ represented by lines which are indented hierarchically.
+- **`tiered_lines_formatter.rb`:**
+ Takes a collection of diff line objects and produces a diff string.
+
+#### Object inspection
+
+- **`inspection_tree_nodes/`:**
+ Classes which represent directives that can be given
+ when constructing an inspection tree.
+ Different directives format the resulting text different ways
+ or even hide text if it does not match a certain condition.
+- **`abstract_inspection_tree_builder.rb`:**
+ The superclass for all inspection tree builder classes,
+ defined elsewhere in SuperDiff or by users.
+- **`inspection_tree.rb`:**
+ A domain-specific language used to format the contents of an object.
+ The result can either be a single-line string,
+ useful for descriptions and failure messages,
+ or a series of Line objects,
+ useful for inserting into diffs.
+- **`inspection_tree_builder_dispatcher.rb`:**
+ Finds the inspection tree builder class that best matches a value,
+ then uses it to build the inspection tree.
+- **`no_inspection_tree_builder_available_error.rb`:**
+ The error produced when the InspectionTreeBuilderDispatcher fails to find a matching class.
+- **`prefix_for_next_inspection_tree_node.rb`:**
+ A special token used to represent
+ the result of the prefix passed to the `as_prefix_when_rendering_to_lines` directive.
+ This prefix needs to be handled specially in the inspection tree rendering logic.
+- **`prelude_for_next_inspection_tree_node.rb`:**
+ A special token used to represent
+ the result of the prelude passed to the `as_prelude_when_rendering_to_lines` directive.
+ This prelude needs to be handled specially in the inspection tree rendering logic.
+- **`tiered_lines.rb`:**
+ Represents the final output of an inspection tree,
+ broken up by lines which are indented hierarchically.
+
+#### Utilities
+
+- **`colorized_document_extensions.rb`:**
+ Extends the ColorizedDocument DSL
+ so that text can be colored
+ using names that are not abstract
+ but rather related to SuperDiff concepts.
+- **`gem_version.rb`:**
+ A wrapper around Gem::Version
+ which simplifies the interface
+ so that operators can be used to compare versions.
+- **`helpers.rb`:**
+ Utility methods which can be mixed in various places to do various things.
+- **`implementation_checks.rb`:**
+ Provides a way to define an unimplemented method.
+ Such a method will raise an error when called
+ unless it is overridden in a superclass.
+- **`recursion_guard.rb`:**
+ A set of globally accessible methods
+ which is used to track repeated encounters of objects
+ as a data structure is descended into
+ and prevent cyclical or infinite recursion.
+
+### Feature modules
+
+There are 4 directories in `lib/super_diff/` which are collectively known as _feature modules_.
+Using the primitives from the Core module,
+they implement building blocks
+to instruct SuperDiff on how to diff many kinds of objects.
+As such, you may see one or more of the following directories:
+
+- `*/differs/`
+- `*/inspection_tree_builders/`
+- `*/operation_tree_builders/`
+- `*/operation_tree_flatteners/`
+- `*/operation_trees/`
+
+With that in mind, here is a list of feature modules.
+
+#### `SuperDiff::Basic` (`lib/super_diff/basic`)
+
+This module is so named
+because it provides functionality that begins to make SuperDiff useful for users
+by adding support for objects that are built into Ruby.
+
+#### `SuperDiff::ActiveSupport` (`lib/super_diff/active_support`)
+
+This module makes use of the building blocks in the Basic module
+to instruct SuperDiff how to diff ActiveSupport-specific objects,
+such as HashWithIndifferentAccess.
+
+#### `SuperDiff::ActiveRecord` (`lib/super_diff/active_record`)
+
+This module makes use of the building blocks in the Basic module
+to instruct SuperDiff how to diff ActiveRecord-specific objects,
+such as models and relation objects.
+
+#### `SuperDiff::RSpec` (`lib/super_diff/rspec`)
+
+This module is the largest one
+and makes use of the building blocks defined by the Basic module
+to provide support for expectation and argument matchers in diffs.
+It also patches sections of RSpec to replace its differ with SuperDiff's
+and to change failure messages so that they fit better with SuperDiff's philosophy.
+
+There are a few files and directories of note here:
+
+- **`matcher_text_builders/`:**
+ These classes are used to craft descriptions and failure messages for RSpec matchers.
+- **`differ.rb`:**
+ This class stands in for RSpec::Differ
+ and conditionally diffs an expected and actual value
+ that has been passed to a matcher.
+- **`matcher_text_template.rb`:**
+ This class is used by matcher text builders
+ to progressively build up a string by concatenating tokens together.
+ It is similar to an inspection tree
+ in that some parts of the string can be rendered differently
+ depending on whether we've chosen to display the message
+ in a single line or across multiple lines.
+ Some tokens can also be colored differently as well.
+- **`monkey_patches.rb`:**
+ The metaphorical skeleton closet,
+ this file does the dirty and outright sinful work
+ of overriding various private APIs of the whole RSpec suite of gems
+ in order to integrate SuperDiff fully into the test framework.
+
+### Equality matchers (`lib/super_diff/equality_matchers/`)
+
+This directory offers a way to try out SuperDiff
+without integrating it into a test framework.
+The classes here are merely thin wrappers around building blocks in the Basic module.
+
+### CSI (`lib/super_diff/csi/`)
+
+This directory contains a small library, private to the codebase,
+which provides a DSL for colorizing text in the running terminal emulator
+with the use of CSI escape sequences.
+
+## Tests
+
+Tests are located in `spec/`.
+They are divided into two kinds:
+
+- **Unit tests**, located in `spec/unit/`,
+ holds tests for individual classes and methods.
+- **Integration tests**, located in `spec/integration/`,
+ construct tiny test suites which import SuperDiff
+ and run them in isolated environments
+ to ensure that SuperDiff works as designed
+ in more real-world scenarios.
+
+The files in `spec/support/` (imported via `spec_helper.rb`)
+contain helpers, matchers, and tests
+that are shared among unit and integration tests,
+at their respective levels.
+
+Beyond this, [Zeus][zeus], a way to speed up integration tests,
+and is run and configured via these files:
+
+- `bin/start-dev`
+- `config/zeus_plan.rb`
+- `support/test_plan.rb`
+- `zeus.json`
+
+The [official Zeus docs](https://github.com/burke/zeus/blob/master/docs/ruby/modifying.md)
+is helpful for understanding how these files work together.
+
+[zeus]: https://github.com/burke/zeus
+
+## Ruby
+
+The following files are used to set up Ruby,
+specify dependencies and gem publishing settings,
+and run tasks:
+
+- **`.ruby-version`:**
+ Specifies the Ruby version required for development.
+ Used by tools like `rbenv` and `asdf.`
+- **`bin/setup`:**
+ Ensures that Ruby, Node, Python, and anything else necessary for development
+ is installed on your machine.
+- **`gemfiles/`:**
+ Holds generated gemspecs and lockfiles for appraisals.
+- **`lib/super_diff/version.rb`:**
+ Holds the current version of the gem,
+ which is used by the gemspec.
+ Updating the constant in this file changes the published version.
+- **`spec/support/current_bundle.rb`:**
+ A support file used by common tasks
+ which tracks the currently set appraisal or sets a default.
+- **`Appraisals`:**
+ Specifies different gemfiles for different testing environments.
+- **`Gemfile`:**
+ Specifies common development dependencies.
+- **`Rakefile`:**
+ Contains tasks for running tests and publishing the gem.
+- **`super_diff.gemspec`:**
+ Standard configuration file for the gem,
+ containing the name, description, and production dependencies.
+
+## Linting
+
+The following files are used for linting and formatting the codebase:
+
+- **`.husky/`:**
+ Contains Git hooks which are installed via `husky install`.
+- **`.yarn/`:**
+ Holds the executable and plugins for [Yarn][yarn],
+ a package manager for JavaScript.
+- **`.prettierignore`:**
+ Excludes files from being formatted with [Prettier][prettier].
+- **`.prettierrc.json`:**
+ Configures Prettier.
+- **`.nvmrc`:**
+ Specifies the Node version required to run Prettier.
+ Used by tools like NVM and `asdf`.
+- **`.yarnrc.yml`:**
+ Configures Yarn.
+- **`package.json`:**
+ Specifies JavaScript dependencies needed to run Prettier.
+
+[prettier]: https://prettier.io/
+[yarn]: https://yarnpkg.com/
+
+## Documentation
+
+The following files are used for documentation purposes:
+
+- **`docs/`:**
+ Contains the documentation that you're reading now!
+- **`docs-support/`:**
+ Other files which are used for documentation purposes,
+ but are not included in any Markdown files.
+ Currently only used to generate the screenshots in the README.
+- **`.python-version`:**
+ Specifies the Python version to use for documentation generation.
+ Used by tools like `pyenv` and `asdf`.
+- **`mkdocs.yml`:**
+ The configuration file for [Mkdocs][mkdocs],
+ which reads the Markdown files in `docs/` and generates an HTML version.
+- **`pyproject.toml`:**
+ Specifies Python dependencies needed to run Mkdocs.
+- **`README.md`:**
+ The "front of house" for this repository;
+ summarizes the project's purpose and goals and how to use it.
+- **`CHANGELOG.md`:**
+ Describes the changes that appeared in each release over time.
+- **`LICENSE`:**
+ The full text of the license granted to users of this gem.
+
+[mkdocs]: https://www.mkdocs.org/
+
+## CI
+
+The following files are used for CI and other automated tasks on GitHub:
+
+- **`.github/workflows/`:**
+ Describes tasks to run on GitHub when events occur,
+ such as automatically running tests on pull requests.
+- **`scripts/collect-release-info.rb`:**
+ A script which is used in one of the GitHub workflows
+ to determine whether a commit refers to a new release
+ and, if so, determine the release version.
+
+## Miscellaneous
+
+These files don't belong in a particular category:
+
+- **`.editorconfig`:**
+ Keeps basic editor-specific configuration,
+ such as how many spaces to use, line length, etc.
+- **`.gitattributes`:**
+ Used by Git and GitHub
+ to configure presentation of certain types of files in diffs.
+- **`.gitignore`:**
+ Specifies files that shouldn't show up in the repository.
diff --git a/docs/contributors/how-to-contribute.md b/docs/contributors/how-to-contribute.md
new file mode 100644
index 00000000..b9d27205
--- /dev/null
+++ b/docs/contributors/how-to-contribute.md
@@ -0,0 +1,129 @@
+# How to Contribute
+
+Want to make a change to this project?
+Great! Here's how you do that.
+
+## 1. Install dependencies
+
+First, [create a fork of the SuperDiff repo](https://github.com/mcmire/super_diff/fork)
+and clone it to your computer.
+
+Next, run the following command in the resulting directory
+in order to install dependencies.
+This will also install a Git hook
+which ensures all code is formatted whenever a commit is pushed:
+
+```
+bin/setup
+```
+
+## 2. Make a new branch
+
+It's best to follow [GitHub Flow][github-flow] when working on this repo.
+Start by making a new branch to hold your changes:
+
+```
+git checkout -b
+```
+
+[github-flow]: https://docs.github.com/en/get-started/using-github/github-flow
+
+## 3. Understand the codebase
+
+Some architectural documents have been provided
+to aid you in understanding the codebase.
+You might find the guide on [how SuperDiff works](./architecture/how-super-diff-works.md) to be helpful, for example.
+
+## 4. Write and run tests
+
+All code is backed by tests,
+so if you want to submit a pull request,
+make sure to update the existing tests or write new ones as you find necessary.
+
+There are two kinds of tests in this project:
+
+- **Unit tests**, kept in `spec/unit`,
+ exercise individual classes and methods in isolation.
+- **Integration tests**, kept in `spec/integration`,
+ exercise the interaction between SuperDiff, RSpec, and parts of Rails.
+
+It's best to run all of the tests after cloning SuperDiff
+to establish a baseline for any changes you want to make,
+but you can also run them at any time:
+
+```
+bundle exec rake
+```
+
+If you want to run one of the tests, say:
+
+```
+bin/rspec spec/integration/...
+bin/rspec spec/unit/...
+```
+
+Note that the integration tests
+can be quite slow to run.
+If you'd like to speed them up,
+run the following command in a separate terminal session:
+
+```
+zeus start
+```
+
+Now the next time you run an integration test by saying
+
+```
+bin/rspec spec/integration/...
+```
+
+it should run twice as fast.
+
+## 5. Run the linter
+
+Code is linted and formatted using Prettier,
+so [make sure that's set up in your editor][prettier-editors].
+If you don't want to do this,
+you can also fix any lint violations by running:
+
+```
+yarn lint:fix
+```
+
+Provided that you ran `bin/setup` above,
+any code you've changed will also be linted
+whenever you push a commit.
+If this step fails for any reason,
+you can fix lint violations in only changed files by running:
+
+```
+yarn lint:changed:fix
+```
+
+[prettier-editors]: https://prettier.io/docs/en/editors.html
+
+## 6. (Optional) Update the documentation
+
+If there's any part of this documentation that you wish to update,
+then in a free terminal session, run:
+
+```
+poetry run mkdocs serve
+```
+
+Now open `http://localhost:8000` to view a preview of the documentation locally.
+
+The files themselves are located in `docs/`
+and are written in Markdown.
+Thanks to the command above,
+updating any of these files will automatically be reflected in the preview.
+
+## 7. Submit a pull request
+
+When you're done,
+push your branch
+and create a new pull request.
+I'll try to respond as quickly as I can.
+I may have suggestions about code style or your approach,
+but hopefully everything looks good and your changes get merged!
+Now you're a contributor! đ
diff --git a/docs/contributors/index.md b/docs/contributors/index.md
new file mode 100644
index 00000000..61087dfe
--- /dev/null
+++ b/docs/contributors/index.md
@@ -0,0 +1,7 @@
+# Contributor Documentation
+
+If you're looking to contribute to SuperDiff's codebase,
+you're in the right place.
+
+Here you can find [instructions on setting up your development environment](./how-to-contribute.md)
+as well as a [guide to the codebase itself](./architecture/introduction.md).
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..c0a60f32
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,14 @@
+---
+hide:
+ - navigation
+---
+
+# SuperDiff
+
+This site hosts user- and contributor-facing documentation for SuperDiff,
+a Ruby gem that hooks into RSpec
+to intelligently display the differences between two data structures of any type.
+
+- [Learn how to use SuperDiff â¤](./users/index.md)
+- [Learn how to contribute to SuperDiff and dive into the source code â¤](./contributors/index.md)
+- [File a bug, submit a feature request, or suggest another change â¤](https://github.com/mcmire/super_diff/issues)
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
new file mode 100644
index 00000000..58a66729
--- /dev/null
+++ b/docs/stylesheets/extra.css
@@ -0,0 +1,20 @@
+.md-typeset h1 {
+ font-size: 2em;
+}
+
+.md-typeset h2 {
+ font-size: 1.7em;
+}
+
+.md-typeset h3 {
+ font-size: 1.445em;
+}
+
+.md-typeset h4 {
+ font-size: 1.23em;
+}
+
+.md-sidebar__inner > .md-nav > .md-nav__list > .md-nav__item > .md-nav__link {
+ text-transform: uppercase;
+ font-size: 0.9em;
+}
diff --git a/docs/users/customization.md b/docs/users/customization.md
new file mode 100644
index 00000000..1cd7a55c
--- /dev/null
+++ b/docs/users/customization.md
@@ -0,0 +1,281 @@
+# Customizing SuperDiff
+
+You can customize the behavior of the gem
+by opening your test helper file
+(`spec/rails_helper.rb` or `spec/spec_helper.rb`)
+and calling `SuperDiff.configure` with a configuration block:
+
+```ruby
+SuperDiff.configure do |config|
+ # ...
+end
+```
+
+The following is a list of options you can set on the configuration object
+along with their defaults:
+
+| name | description | default |
+| ---------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------- |
+| `actual_color` | The color used to display "actual" values in diffs | `:yellow` |
+| `border_color` | The color used to display the border in diff keys | `:blue` |
+| `color_enabled` | Whether to colorize output | `true` if `ENV["CI"]` or stdout is a TTY, `false` otherwise |
+| `diff_elision_enabled` | Whether to elide (remove) unchanged lines in diff | `false` |
+| `diff_elision_maximum` | How large a section of consecutive unchanged lines can be before being elided | `0` |
+| `elision_marker_color` | The color used to display the marker substituted for elided lines in a diff | `:cyan` |
+| `expected_color` | The color used to display "expected" values in diffs | `:magenta` |
+| `header_color` | The color used to display the "Diff:" header in failure messages | `:white` |
+| `key_enabled` | Whether to show the key above diffs | `true` |
+
+The following is a list of methods you can call on the configuration object:
+
+| name | description |
+| ------------------------------------------- | ------------------------------------------------------------------- |
+| `add_extra_diff_formatter_classes` | Additional classes with which to format diffs |
+| `add_extra_differ_classes` | Additional classes with which to compute diffs for objects |
+| `add_extra_inspection_tree_builder_classes` | Additional classes used to inspect objects |
+| `add_extra_operation_tree_builder_classes` | Additional classes used to build operation trees for objects |
+| `add_extra_operation_tree_classes` | Additional classes used to hold operations in diffs between objects |
+
+Read on for more information about available kinds of customizations.
+
+### Customizing colors
+
+If you don't like the colors that SuperDiff uses,
+you can change them like so:
+
+```ruby
+SuperDiff.configure do |config|
+ config.actual_color = :green
+ config.expected_color = :red
+ config.border_color = :yellow
+ config.header_color = :yellow
+end
+```
+
+See `CSI::EightBitColor` in the codebase
+for the list of available colors you can use as values here.
+
+You can also completely disable colorized output:
+
+```ruby
+SuperDiff.configure { |config| config.color_enabled = false }
+```
+
+### Disabling the key
+
+By default, when a diff is displayed,
+a key appears above it.
+This key serves to clarify
+which colors and symbols belong to the "expected" and "actual" values.
+However, you can disable the key as follows:
+
+```ruby
+SuperDiff.configure { |config| config.key_enabled = false }
+```
+
+### Hiding unchanged lines
+
+When looking at a large diff made up of many lines that do not change,
+it can be difficult to make out the lines that do.
+Text-oriented diffs,
+such as those you get from a conventional version control system,
+solve this problem by removing or "eliding" those unchanged lines from the diff entirely.
+The same can be done in SuperDiff.
+
+For instance, the following configuration enables diff elision
+and ensures that within a block of unchanged lines,
+a maximum of only 3 lines are displayed:
+
+```ruby
+SuperDiff.configure do |config|
+ config.diff_elision_enabled = true
+ config.diff_elision_maximum = 3
+end
+```
+
+A diff in which some lines are elided may look like this:
+
+```diff
+ [
+ # ...
+ "American Samoa",
+ "Andorra",
+- "Angola",
++ "Anguilla",
+ "Antarctica",
+ "Antigua And Barbuda",
+ # ...
+ ]
+```
+
+as opposed to:
+
+```diff
+ [
+ "Afghanistan",
+ "Aland Islands",
+ "Albania",
+ "Algeria",
+ "American Samoa",
+ "Andorra",
+- "Angola",
++ "Anguilla",
+ "Antarctica",
+ "Antigua And Barbuda",
+ "Argentina",
+ "Armenia",
+ "Aruba",
+ "Australia"
+ ]
+```
+
+### Diffing custom objects
+
+If you are comparing two instances of a class
+which are specific to your project,
+the resulting diff may not look as good
+as diffs involving native or primitive objects.
+This happens because if SuperDiff doesn't recognize a class,
+it will fall back to a generic representation for the diff.
+
+There are two ways to solve this problem.
+
+#### Adding an `attributes_for_super_diff` method
+
+This is the easiest approach.
+If two objects have this method,
+SuperDiff will use the hash that this method returns to compare those objects
+and will compute a diff between them,
+which will show up in the output.
+
+##### Example
+
+For instance, say we have the following classes:
+
+```ruby
+class Http
+ # ...
+end
+
+class Order
+ def initialize(id, number)
+ @id = id
+ @number = number
+ end
+end
+
+class OrderRequestor
+ def initialize(order)
+ @order = order
+ @http_library = Http.new
+ end
+
+ def request
+ @http_library.get("/orders/#{order.id}")
+ end
+end
+
+class OrderTracker
+ def initialize(order)
+ @order = order
+ @requestor = OrderRequestor.new(order)
+ end
+end
+```
+
+and we have two instances of these class as follows:
+
+```ruby
+actual = OrderTracker.new(Order.new(id: 1, number: "1000"))
+expected = OrderTracker.new(Order.new(id: 2, number: "2000"))
+```
+
+If we diff these two objects,
+then we will see something like:
+
+```diff
+ #,
+- @requestor=#,
+- @http_library=#
+- }>
++ @order=#,
++ @requestor=#,
++ @http_library=#
++ }>
+ }>
+```
+
+It is not difficult to see that this diff is fairly noisy.
+It would be good if we could exclude `requestor`,
+since it's a bit redundant,
+and it would help if we could collapse some of the lines as well.
+We also don't need to know the address of each object
+(the `0xXXXXXXXXX` bit).
+
+We can easily solve this
+by adding an `attributes_for_super_diff` method to OrderTracker,
+making sure to exclude `requestor`,
+and by adding a similar method to Order as well.
+
+```diff
+ class Order
+ def initialize(id, number)
+ @id = id
+ @number = number
+ end
++
++ def attributes_for_super_diff
++ { id: @id, number: @number }
++ end
+ end
+
+ class OrderTracker
+ def initialize(order)
+ @order = order
+ @requestor = OrderRequestor.new(order)
+ end
++
++ def attributes_for_super_diff
++ { order: @order }
++ end
+ end
+```
+
+If we performed another diff, we would now get:
+
+```diff
+ #
+ }>
+```
+
+#### Registering new building blocks
+
+This approach is more advanced,
+but also offers the greatest flexibility.
+
+More information will be added here on how to do this,
+but in the meantime,
+the best example is the [RSpec integration](https://github.com/mcmire/super_diff/blob/v0.11.0/lib/super_diff/rspec.rb#L91) in SuperDiff itself.
diff --git a/docs/users/getting-started.md b/docs/users/getting-started.md
new file mode 100644
index 00000000..019b1ec5
--- /dev/null
+++ b/docs/users/getting-started.md
@@ -0,0 +1,118 @@
+# Getting Started
+
+SuperDiff is designed to be used different ways, depending on the type of project.
+
+## Using SuperDiff with RSpec in a Rails app
+
+If you're developing a Rails app,
+run the following command to add SuperDiff to your project:
+
+```bash
+bundle add super_diff --group test
+```
+
+Then add the following toward the top of `spec/rails_helper.rb`:
+
+```ruby
+require "super_diff/rspec-rails"
+```
+
+At this point, you can write tests for parts of your app,
+and SuperDiff will be able to diff Rails-specific objects
+such as ActiveRecord models,
+ActionController response objects,
+instances of HashWithIndifferentAccess, etc.,
+in addition to objects that ship with RSpec,
+such as matchers.
+
+You can now continue on to [customizing SuperDiff](./customization.md).
+
+## Using SuperDiff with RSpec in a project using parts of Rails
+
+If you're developing an app using Hanami or Sinatra,
+or merely using a part of Rails such as ActiveModel,
+run the following command to add SuperDiff to your project:
+
+```bash
+bundle add super_diff
+```
+
+After running `bundle install`,
+add the following toward the top of `spec/spec_helper.rb`:
+
+```ruby
+require "super_diff/rspec"
+```
+
+Then, add one or all of the following lines:
+
+```ruby
+require "super_diff/active_support"
+require "super_diff/active_record"
+```
+
+At this point, you can write tests for parts of your app,
+and SuperDiff will be able to diff objects depending on which path you required.
+For instance, if you required `super_diff/active_support`,
+then SuperDiff will be able to diff objects defined in ActiveSupport,
+such as HashWithIndifferentAccess,
+and if you required `super_diff/active_record`,
+it will be able to diff ActiveRecord models.
+In addition to these,
+it will also be able to diff objects that ship with RSpec,
+such as matchers.
+
+You can now continue on to [customizing SuperDiff](./customization.md).
+
+## Using SuperDiff with RSpec in a Ruby project
+
+If you're developing a library or other project
+that does not depend on any part of Rails,
+run the following command to add SuperDiff to your project:
+
+```bash
+bundle add super_diff
+```
+
+Now add the following toward the top of `spec/spec_helper.rb`:
+
+```ruby
+require "super_diff/rspec"
+```
+
+At this point, you can write tests for parts of your app,
+and SuperDiff will be able to diff objects that ship with RSpec,
+such as matchers.
+
+You can now continue on to [customizing SuperDiff](./customization.md).
+
+## Using parts of SuperDiff directly
+
+Although SuperDiff is primarily designed to integrate with RSpec,
+it can also be used on its own in other kinds of applications.
+
+First, install the gem:
+
+```bash
+bundle add super_diff
+```
+
+Then, require it somewhere:
+
+```ruby
+require "super_diff"
+```
+
+If you want to compare two objects and display a friendly diff,
+you can use the equality matcher interface:
+
+```ruby
+SuperDiff::EqualityMatchers::Main.call(expected, actual)
+```
+
+Or, if you want to compare two objects and get a lower-level list of operations,
+you can use the differ interface:
+
+```ruby
+SuperDiff::Differs::Main.call(expected, actual)
+```
diff --git a/docs/users/index.md b/docs/users/index.md
new file mode 100644
index 00000000..e2624697
--- /dev/null
+++ b/docs/users/index.md
@@ -0,0 +1,74 @@
+# Introduction to SuperDiff
+
+**SuperDiff** is a Ruby gem
+which is designed to display the differences between two objects of any type
+in a familiar and intelligent fashion.
+
+The primary motivation behind this gem
+is to vastly improve upon RSpec's built-in diffing capabilities.
+RSpec has many nice features,
+and one of them is that whenever you use a matcher such as `eq`, `match`, `include`, or `have_attributes`,
+you will get a diff of the two data structures you are trying to match against.
+This is great if all you want to do is compare multi-line strings.
+But if you want to compare other, more "real world" kinds of values such as API or database data,
+then you are out of luck.
+Since [RSpec merely runs your `expected` and `actual` values through Ruby's PrettyPrinter library][rspec-differ-fail]
+and then performs a diff of these strings,
+the output it produces leaves much to be desired.
+
+[rspec-differ-fail]: https://github.com/rspec/rspec-support/blob/c69a231d7369dd165ad7ce4742e1a2e21e3462b5/lib/rspec/support/differ.rb#L178
+
+For instance, let's say you wanted to compare these two hashes:
+
+```ruby
+actual = {
+ customer: {
+ person: SuperDiff::Test::Person.new(name: "Marty McFly, Jr.", age: 17),
+ shipping_address: {
+ line_1: "456 Ponderosa Ct.",
+ city: "Hill Valley",
+ state: "CA",
+ zip: "90382"
+ }
+ },
+ items: [
+ { name: "Fender Stratocaster", cost: 100_000, options: %w[red blue green] },
+ { name: "Mattel Hoverboard" }
+ ]
+}
+
+expected = {
+ customer: {
+ person: SuperDiff::Test::Person.new(name: "Marty McFly", age: 17),
+ shipping_address: {
+ line_1: "123 Main St.",
+ city: "Hill Valley",
+ state: "CA",
+ zip: "90382"
+ }
+ },
+ items: [
+ { name: "Fender Stratocaster", cost: 100_000, options: %w[red blue green] },
+ { name: "Chevy 4x4" }
+ ]
+}
+```
+
+If, somewhere in a test, you were to say:
+
+```ruby
+expect(actual).to eq(expected)
+```
+
+You would get output that looks like this:
+
+
+
+What this library does
+is to provide a diff engine
+that knows how to figure out the differences between any two data structures
+and display them in a sensible way.
+So, using the example above,
+you'd get this instead:
+
+
diff --git a/lib/super_diff.rb b/lib/super_diff.rb
index a2118b25..349f9b52 100644
--- a/lib/super_diff.rb
+++ b/lib/super_diff.rb
@@ -1,47 +1,93 @@
require "attr_extras/explicit"
-require "diff-lcs"
-require "patience_diff"
require "date"
module SuperDiff
- autoload(
- :ColorizedDocumentExtensions,
- "super_diff/colorized_document_extensions"
- )
- autoload :OperationTreeFlatteners, "super_diff/operation_tree_flatteners"
- autoload :Configuration, "super_diff/configuration"
+ autoload :Core, "super_diff/core"
autoload :Csi, "super_diff/csi"
- autoload :DiffFormatters, "super_diff/diff_formatters"
autoload :Differs, "super_diff/differs"
autoload :EqualityMatchers, "super_diff/equality_matchers"
autoload :Errors, "super_diff/errors"
- autoload :GemVersion, "super_diff/gem_version"
- autoload :Helpers, "super_diff/helpers"
- autoload :ImplementationChecks, "super_diff/implementation_checks"
- autoload :Line, "super_diff/line"
- autoload :TieredLines, "super_diff/tiered_lines"
- autoload :TieredLinesElider, "super_diff/tiered_lines_elider"
- autoload :TieredLinesFormatter, "super_diff/tiered_lines_formatter"
autoload :ObjectInspection, "super_diff/object_inspection"
- autoload :OperationTrees, "super_diff/operation_trees"
autoload :OperationTreeBuilders, "super_diff/operation_tree_builders"
+ autoload :OperationTreeFlatteners, "super_diff/operation_tree_flatteners"
+ autoload :OperationTrees, "super_diff/operation_trees"
autoload :Operations, "super_diff/operations"
- autoload :RecursionGuard, "super_diff/recursion_guard"
autoload :VERSION, "super_diff/version"
+ def self.const_missing(missing_const_name)
+ if Core.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core.const_get(missing_const_name)
+ elsif Basic.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Basic::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Basic.const_get(missing_const_name)
+ else
+ super
+ end
+ end
+
def self.configure
yield configuration
configuration.updated
end
def self.configuration
- @_configuration ||= Configuration.new
+ @_configuration ||= Core::Configuration.new
+ end
+
+ def self.diff(
+ expected,
+ actual,
+ indent_level: 0,
+ raise_if_nothing_applies: true
+ )
+ Core::DifferDispatcher.call(
+ expected,
+ actual,
+ available_classes: configuration.extra_differ_classes,
+ indent_level: indent_level,
+ raise_if_nothing_applies: raise_if_nothing_applies
+ )
+ end
+
+ def self.build_operation_tree_for(
+ expected,
+ actual,
+ extra_operation_tree_builder_classes: [],
+ raise_if_nothing_applies: false
+ )
+ Core::OperationTreeBuilderDispatcher.call(
+ expected,
+ actual,
+ available_classes:
+ configuration.extra_operation_tree_builder_classes +
+ extra_operation_tree_builder_classes,
+ raise_if_nothing_applies: raise_if_nothing_applies
+ )
+ end
+
+ def self.find_operation_tree_for(value)
+ SuperDiff::Core::OperationTreeFinder.call(
+ value,
+ available_classes: configuration.extra_operation_tree_classes
+ )
end
def self.inspect_object(object, as_lines:, **rest)
- SuperDiff::RecursionGuard.guarding_recursion_of(object) do
+ Core::RecursionGuard.guarding_recursion_of(object) do
inspection_tree =
- ObjectInspection::InspectionTreeBuilders::Main.call(object)
+ Core::InspectionTreeBuilderDispatcher.call(
+ object,
+ available_classes: configuration.extra_inspection_tree_builder_classes
+ )
if as_lines
inspection_tree.render_to_lines(object, **rest)
@@ -87,3 +133,5 @@ def self.insert_singleton_overrides(target_module, mod = nil, &block)
end
end
end
+
+require "super_diff/basic"
diff --git a/lib/super_diff/active_record.rb b/lib/super_diff/active_record.rb
index 5c0fb123..f092b798 100644
--- a/lib/super_diff/active_record.rb
+++ b/lib/super_diff/active_record.rb
@@ -1,28 +1,24 @@
require "super_diff/active_support"
+require "super_diff/active_record/differs"
+require "super_diff/active_record/inspection_tree_builders"
+require "super_diff/active_record/operation_trees"
+require "super_diff/active_record/operation_tree_builders"
+require "super_diff/active_record/operation_tree_flatteners"
+
module SuperDiff
module ActiveRecord
- autoload :Differs, "super_diff/active_record/differs"
- autoload(:ObjectInspection, "super_diff/active_record/object_inspection")
- autoload(:OperationTrees, "super_diff/active_record/operation_trees")
- autoload(
- :OperationTreeBuilders,
- "super_diff/active_record/operation_tree_builders"
- )
- autoload(
- :OperationTreeFlatteners,
- "super_diff/active_record/operation_tree_flatteners"
- )
+ autoload :ObjectInspection, "super_diff/active_record/object_inspection"
SuperDiff.configure do |config|
- config.add_extra_differ_classes(Differs::ActiveRecordRelation)
- config.add_extra_operation_tree_builder_classes(
+ config.prepend_extra_differ_classes(Differs::ActiveRecordRelation)
+ config.prepend_extra_operation_tree_builder_classes(
OperationTreeBuilders::ActiveRecordModel,
OperationTreeBuilders::ActiveRecordRelation
)
- config.add_extra_inspection_tree_builder_classes(
- ObjectInspection::InspectionTreeBuilders::ActiveRecordModel,
- ObjectInspection::InspectionTreeBuilders::ActiveRecordRelation
+ config.prepend_extra_inspection_tree_builder_classes(
+ InspectionTreeBuilders::ActiveRecordModel,
+ InspectionTreeBuilders::ActiveRecordRelation
)
end
end
diff --git a/lib/super_diff/active_record/differs/active_record_relation.rb b/lib/super_diff/active_record/differs/active_record_relation.rb
index 0c1e5467..d8eb9170 100644
--- a/lib/super_diff/active_record/differs/active_record_relation.rb
+++ b/lib/super_diff/active_record/differs/active_record_relation.rb
@@ -1,7 +1,7 @@
module SuperDiff
module ActiveRecord
module Differs
- class ActiveRecordRelation < SuperDiff::Differs::Base
+ class ActiveRecordRelation < Core::AbstractDiffer
def self.applies_to?(expected, actual)
expected.is_a?(::Array) && actual.is_a?(::ActiveRecord::Relation)
end
diff --git a/lib/super_diff/active_record/inspection_tree_builders.rb b/lib/super_diff/active_record/inspection_tree_builders.rb
new file mode 100644
index 00000000..19673c24
--- /dev/null
+++ b/lib/super_diff/active_record/inspection_tree_builders.rb
@@ -0,0 +1,14 @@
+module SuperDiff
+ module ActiveRecord
+ module InspectionTreeBuilders
+ autoload(
+ :ActiveRecordModel,
+ "super_diff/active_record/inspection_tree_builders/active_record_model"
+ )
+ autoload(
+ :ActiveRecordRelation,
+ "super_diff/active_record/inspection_tree_builders/active_record_relation"
+ )
+ end
+ end
+end
diff --git a/lib/super_diff/active_record/inspection_tree_builders/active_record_model.rb b/lib/super_diff/active_record/inspection_tree_builders/active_record_model.rb
new file mode 100644
index 00000000..84929abf
--- /dev/null
+++ b/lib/super_diff/active_record/inspection_tree_builders/active_record_model.rb
@@ -0,0 +1,57 @@
+module SuperDiff
+ module ActiveRecord
+ module InspectionTreeBuilders
+ class ActiveRecordModel < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ value.is_a?(::ActiveRecord::Base)
+ end
+
+ def id
+ object.class.primary_key
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ t1.as_lines_when_rendering_to_lines(
+ collection_bookend: :open
+ ) do |t2|
+ t2.add_text "#<#{object.class} "
+
+ # stree-ignore
+ t2.when_rendering_to_lines do |t3|
+ t3.add_text "{"
+ end
+ end
+
+ t1.nested do |t2|
+ t2.insert_separated_list(
+ [id] + (object.attributes.keys.sort - [id])
+ ) do |t3, name|
+ t3.as_prefix_when_rendering_to_lines do |t4|
+ t4.add_text "#{name}: "
+ end
+
+ if name == id
+ t3.add_inspection_of object.id
+ else
+ t3.add_inspection_of object.read_attribute(name)
+ end
+ end
+ end
+
+ t1.as_lines_when_rendering_to_lines(
+ collection_bookend: :close
+ ) do |t2|
+ # stree-ignore
+ t2.when_rendering_to_lines do |t3|
+ t3.add_text "}"
+ end
+
+ t2.add_text ">"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/active_record/inspection_tree_builders/active_record_relation.rb b/lib/super_diff/active_record/inspection_tree_builders/active_record_relation.rb
new file mode 100644
index 00000000..5380b00c
--- /dev/null
+++ b/lib/super_diff/active_record/inspection_tree_builders/active_record_relation.rb
@@ -0,0 +1,34 @@
+module SuperDiff
+ module ActiveRecord
+ module InspectionTreeBuilders
+ class ActiveRecordRelation < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ value.is_a?(::ActiveRecord::Relation)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ # stree-ignore
+ t1.as_lines_when_rendering_to_lines(
+ collection_bookend: :open
+ ) do |t2|
+ t2.add_text "#"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/active_record/monkey_patches.rb b/lib/super_diff/active_record/monkey_patches.rb
index 05274a9c..52ae6634 100644
--- a/lib/super_diff/active_record/monkey_patches.rb
+++ b/lib/super_diff/active_record/monkey_patches.rb
@@ -1,9 +1,12 @@
# rubocop:disable Style/BracesAroundHashParameters, Style/ClassAndModuleChildren
class ActiveRecord::Base
+ # TODO: Remove this monkey patch if possible
def attributes_for_super_diff
- (attributes.keys.sort - ["id"]).reduce({ id: id }) do |hash, key|
- hash.merge(key.to_sym => attributes[key])
- end
+ id_attr = self.class.primary_key
+
+ (attributes.keys.sort - [id_attr]).reduce(
+ { id_attr.to_sym => id }
+ ) { |hash, key| hash.merge(key.to_sym => attributes[key]) }
end
end
# rubocop:enable Style/BracesAroundHashParameters, Style/ClassAndModuleChildren
diff --git a/lib/super_diff/active_record/object_inspection.rb b/lib/super_diff/active_record/object_inspection.rb
index 341938ea..6d300c47 100644
--- a/lib/super_diff/active_record/object_inspection.rb
+++ b/lib/super_diff/active_record/object_inspection.rb
@@ -1,10 +1,22 @@
module SuperDiff
module ActiveRecord
module ObjectInspection
- autoload(
- :InspectionTreeBuilders,
- "super_diff/active_record/object_inspection/inspection_tree_builders"
- )
+ module InspectionTreeBuilders
+ def self.const_missing(missing_const_name)
+ if ActiveRecord::InspectionTreeBuilders.const_defined?(
+ missing_const_name
+ )
+ warn <<~EOT
+ WARNING: SuperDiff::ActiveRecord::ObjectInspection::InspectionTreeBuilders::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::ActiveRecord::InspectionTreeBuilders::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ ActiveRecord::InspectionTreeBuilders.const_get(missing_const_name)
+ else
+ super
+ end
+ end
+ end
end
end
end
diff --git a/lib/super_diff/active_record/object_inspection/inspection_tree_builders.rb b/lib/super_diff/active_record/object_inspection/inspection_tree_builders.rb
deleted file mode 100644
index 40ef6905..00000000
--- a/lib/super_diff/active_record/object_inspection/inspection_tree_builders.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module SuperDiff
- module ActiveRecord
- module ObjectInspection
- module InspectionTreeBuilders
- autoload(
- :ActiveRecordModel,
- "super_diff/active_record/object_inspection/inspection_tree_builders/active_record_model"
- )
- autoload(
- :ActiveRecordRelation,
- "super_diff/active_record/object_inspection/inspection_tree_builders/active_record_relation"
- )
- end
- end
- end
-end
diff --git a/lib/super_diff/active_record/object_inspection/inspection_tree_builders/active_record_model.rb b/lib/super_diff/active_record/object_inspection/inspection_tree_builders/active_record_model.rb
deleted file mode 100644
index a3b79766..00000000
--- a/lib/super_diff/active_record/object_inspection/inspection_tree_builders/active_record_model.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-module SuperDiff
- module ActiveRecord
- module ObjectInspection
- module InspectionTreeBuilders
- class ActiveRecordModel < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- value.is_a?(::ActiveRecord::Base)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- t1.as_lines_when_rendering_to_lines(
- collection_bookend: :open
- ) do |t2|
- t2.add_text "#<#{object.class} "
-
- # stree-ignore
- t2.when_rendering_to_lines do |t3|
- t3.add_text "{"
- end
- end
-
- t1.nested do |t2|
- t2.insert_separated_list(
- ["id"] + (object.attributes.keys.sort - ["id"])
- ) do |t3, name|
- t3.as_prefix_when_rendering_to_lines do |t4|
- t4.add_text "#{name}: "
- end
-
- t3.add_inspection_of object.read_attribute(name)
- end
- end
-
- t1.as_lines_when_rendering_to_lines(
- collection_bookend: :close
- ) do |t2|
- # stree-ignore
- t2.when_rendering_to_lines do |t3|
- t3.add_text "}"
- end
-
- t2.add_text ">"
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/active_record/object_inspection/inspection_tree_builders/active_record_relation.rb b/lib/super_diff/active_record/object_inspection/inspection_tree_builders/active_record_relation.rb
deleted file mode 100644
index d85167e7..00000000
--- a/lib/super_diff/active_record/object_inspection/inspection_tree_builders/active_record_relation.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module SuperDiff
- module ActiveRecord
- module ObjectInspection
- module InspectionTreeBuilders
- class ActiveRecordRelation < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- value.is_a?(::ActiveRecord::Relation)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- # stree-ignore
- t1.as_lines_when_rendering_to_lines(
- collection_bookend: :open
- ) do |t2|
- t2.add_text "#"
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/active_record/operation_tree_builders/active_record_model.rb b/lib/super_diff/active_record/operation_tree_builders/active_record_model.rb
index 6d22196c..832462ea 100644
--- a/lib/super_diff/active_record/operation_tree_builders/active_record_model.rb
+++ b/lib/super_diff/active_record/operation_tree_builders/active_record_model.rb
@@ -1,7 +1,7 @@
module SuperDiff
module ActiveRecord
module OperationTreeBuilders
- class ActiveRecordModel < SuperDiff::OperationTreeBuilders::CustomObject
+ class ActiveRecordModel < Basic::OperationTreeBuilders::CustomObject
def self.applies_to?(expected, actual)
expected.is_a?(::ActiveRecord::Base) &&
actual.is_a?(::ActiveRecord::Base) && expected.class == actual.class
@@ -9,8 +9,12 @@ def self.applies_to?(expected, actual)
protected
+ def id
+ expected.class.primary_key
+ end
+
def attribute_names
- ["id"] + (expected.attributes.keys.sort - ["id"])
+ [id] + (expected.attributes.keys.sort - [id])
end
end
end
diff --git a/lib/super_diff/active_record/operation_tree_builders/active_record_relation.rb b/lib/super_diff/active_record/operation_tree_builders/active_record_relation.rb
index 1752966d..9a991e1c 100644
--- a/lib/super_diff/active_record/operation_tree_builders/active_record_relation.rb
+++ b/lib/super_diff/active_record/operation_tree_builders/active_record_relation.rb
@@ -1,7 +1,7 @@
module SuperDiff
module ActiveRecord
module OperationTreeBuilders
- class ActiveRecordRelation < SuperDiff::OperationTreeBuilders::Array
+ class ActiveRecordRelation < Basic::OperationTreeBuilders::Array
def self.applies_to?(expected, actual)
expected.is_a?(::Array) && actual.is_a?(::ActiveRecord::Relation)
end
diff --git a/lib/super_diff/active_record/operation_tree_flatteners/active_record_relation.rb b/lib/super_diff/active_record/operation_tree_flatteners/active_record_relation.rb
index fda4b6b0..29e4069d 100644
--- a/lib/super_diff/active_record/operation_tree_flatteners/active_record_relation.rb
+++ b/lib/super_diff/active_record/operation_tree_flatteners/active_record_relation.rb
@@ -1,7 +1,7 @@
module SuperDiff
module ActiveRecord
module OperationTreeFlatteners
- class ActiveRecordRelation < SuperDiff::OperationTreeFlatteners::Collection
+ class ActiveRecordRelation < Basic::OperationTreeFlatteners::Collection
protected
def open_token
diff --git a/lib/super_diff/active_record/operation_trees/active_record_relation.rb b/lib/super_diff/active_record/operation_trees/active_record_relation.rb
index 53a9ae37..e5176426 100644
--- a/lib/super_diff/active_record/operation_trees/active_record_relation.rb
+++ b/lib/super_diff/active_record/operation_trees/active_record_relation.rb
@@ -1,7 +1,7 @@
module SuperDiff
module ActiveRecord
module OperationTrees
- class ActiveRecordRelation < SuperDiff::OperationTrees::Array
+ class ActiveRecordRelation < Basic::OperationTrees::Array
def self.applies_to?(value)
value.is_a?(ActiveRecord::Relation)
end
diff --git a/lib/super_diff/active_support.rb b/lib/super_diff/active_support.rb
index c907c1b2..5e69bb40 100644
--- a/lib/super_diff/active_support.rb
+++ b/lib/super_diff/active_support.rb
@@ -1,25 +1,21 @@
+require "super_diff/active_support/differs"
+require "super_diff/active_support/inspection_tree_builders"
+require "super_diff/active_support/operation_trees"
+require "super_diff/active_support/operation_tree_builders"
+require "super_diff/active_support/operation_tree_flatteners"
+
module SuperDiff
module ActiveSupport
- autoload :Differs, "super_diff/active_support/differs"
autoload :ObjectInspection, "super_diff/active_support/object_inspection"
- autoload(:OperationTrees, "super_diff/active_support/operation_trees")
- autoload(
- :OperationTreeBuilders,
- "super_diff/active_support/operation_tree_builders"
- )
- autoload(
- :OperationTreeFlatteners,
- "super_diff/active_support/operation_tree_flatteners"
- )
SuperDiff.configure do |config|
- config.add_extra_differ_classes(Differs::HashWithIndifferentAccess)
- config.add_extra_operation_tree_builder_classes(
+ config.prepend_extra_differ_classes(Differs::HashWithIndifferentAccess)
+ config.prepend_extra_operation_tree_builder_classes(
OperationTreeBuilders::HashWithIndifferentAccess
)
- config.add_extra_inspection_tree_builder_classes(
- ObjectInspection::InspectionTreeBuilders::HashWithIndifferentAccess,
- ObjectInspection::InspectionTreeBuilders::OrderedOptions
+ config.prepend_extra_inspection_tree_builder_classes(
+ InspectionTreeBuilders::HashWithIndifferentAccess,
+ InspectionTreeBuilders::OrderedOptions
)
end
end
diff --git a/lib/super_diff/active_support/differs/hash_with_indifferent_access.rb b/lib/super_diff/active_support/differs/hash_with_indifferent_access.rb
index 2a9fc3a2..fc00551a 100644
--- a/lib/super_diff/active_support/differs/hash_with_indifferent_access.rb
+++ b/lib/super_diff/active_support/differs/hash_with_indifferent_access.rb
@@ -1,7 +1,7 @@
module SuperDiff
module ActiveSupport
module Differs
- class HashWithIndifferentAccess < SuperDiff::Differs::Hash
+ class HashWithIndifferentAccess < Basic::Differs::Hash
def self.applies_to?(expected, actual)
(
expected.is_a?(::HashWithIndifferentAccess) && actual.is_a?(::Hash)
diff --git a/lib/super_diff/active_support/inspection_tree_builders.rb b/lib/super_diff/active_support/inspection_tree_builders.rb
new file mode 100644
index 00000000..7247f255
--- /dev/null
+++ b/lib/super_diff/active_support/inspection_tree_builders.rb
@@ -0,0 +1,14 @@
+module SuperDiff
+ module ActiveSupport
+ module InspectionTreeBuilders
+ autoload(
+ :HashWithIndifferentAccess,
+ "super_diff/active_support/inspection_tree_builders/hash_with_indifferent_access"
+ )
+ autoload(
+ :OrderedOptions,
+ "super_diff/active_support/inspection_tree_builders/ordered_options"
+ )
+ end
+ end
+end
diff --git a/lib/super_diff/active_support/inspection_tree_builders/hash_with_indifferent_access.rb b/lib/super_diff/active_support/inspection_tree_builders/hash_with_indifferent_access.rb
new file mode 100644
index 00000000..0c7bf646
--- /dev/null
+++ b/lib/super_diff/active_support/inspection_tree_builders/hash_with_indifferent_access.rb
@@ -0,0 +1,44 @@
+module SuperDiff
+ module ActiveSupport
+ module InspectionTreeBuilders
+ class HashWithIndifferentAccess < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ value.is_a?(::HashWithIndifferentAccess)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ # stree-ignore
+ t1.as_lines_when_rendering_to_lines(
+ collection_bookend: :open
+ ) do |t2|
+ t2.add_text "#"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/active_support/inspection_tree_builders/ordered_options.rb b/lib/super_diff/active_support/inspection_tree_builders/ordered_options.rb
new file mode 100644
index 00000000..83446115
--- /dev/null
+++ b/lib/super_diff/active_support/inspection_tree_builders/ordered_options.rb
@@ -0,0 +1,44 @@
+module SuperDiff
+ module ActiveSupport
+ module InspectionTreeBuilders
+ class OrderedOptions < Basic::InspectionTreeBuilders::Hash
+ def self.applies_to?(value)
+ value.is_a?(::ActiveSupport::OrderedOptions)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ # stree-ignore
+ t1.as_lines_when_rendering_to_lines(
+ collection_bookend: :open
+ ) do |t2|
+ t2.add_text "#"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/active_support/object_inspection.rb b/lib/super_diff/active_support/object_inspection.rb
index 66e0e1f7..6deffb77 100644
--- a/lib/super_diff/active_support/object_inspection.rb
+++ b/lib/super_diff/active_support/object_inspection.rb
@@ -1,10 +1,22 @@
module SuperDiff
module ActiveSupport
module ObjectInspection
- autoload(
- :InspectionTreeBuilders,
- "super_diff/active_support/object_inspection/inspection_tree_builders"
- )
+ module InspectionTreeBuilders
+ def self.const_missing(missing_const_name)
+ if ActiveSupport::InspectionTreeBuilders.const_defined?(
+ missing_const_name
+ )
+ warn <<~EOT
+ WARNING: SuperDiff::ActiveSupport::ObjectInspection::InspectionTreeBuilders::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::ActiveSupport::InspectionTreeBuilders::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ ActiveSupport::InspectionTreeBuilders.const_get(missing_const_name)
+ else
+ super
+ end
+ end
+ end
end
end
end
diff --git a/lib/super_diff/active_support/object_inspection/inspection_tree_builders.rb b/lib/super_diff/active_support/object_inspection/inspection_tree_builders.rb
deleted file mode 100644
index 412218e3..00000000
--- a/lib/super_diff/active_support/object_inspection/inspection_tree_builders.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module SuperDiff
- module ActiveSupport
- module ObjectInspection
- module InspectionTreeBuilders
- autoload(
- :HashWithIndifferentAccess,
- "super_diff/active_support/object_inspection/inspection_tree_builders/hash_with_indifferent_access"
- )
- autoload(
- :OrderedOptions,
- "super_diff/active_support/object_inspection/inspection_tree_builders/ordered_options"
- )
- end
- end
- end
-end
diff --git a/lib/super_diff/active_support/object_inspection/inspection_tree_builders/hash_with_indifferent_access.rb b/lib/super_diff/active_support/object_inspection/inspection_tree_builders/hash_with_indifferent_access.rb
deleted file mode 100644
index 9af7448f..00000000
--- a/lib/super_diff/active_support/object_inspection/inspection_tree_builders/hash_with_indifferent_access.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-module SuperDiff
- module ActiveSupport
- module ObjectInspection
- module InspectionTreeBuilders
- class HashWithIndifferentAccess < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- value.is_a?(::HashWithIndifferentAccess)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- # stree-ignore
- t1.as_lines_when_rendering_to_lines(
- collection_bookend: :open
- ) do |t2|
- t2.add_text "#"
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/active_support/object_inspection/inspection_tree_builders/ordered_options.rb b/lib/super_diff/active_support/object_inspection/inspection_tree_builders/ordered_options.rb
deleted file mode 100644
index 730d3817..00000000
--- a/lib/super_diff/active_support/object_inspection/inspection_tree_builders/ordered_options.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-module SuperDiff
- module ActiveSupport
- module ObjectInspection
- module InspectionTreeBuilders
- class OrderedOptions < SuperDiff::ObjectInspection::InspectionTreeBuilders::Hash
- def self.applies_to?(value)
- value.is_a?(::ActiveSupport::OrderedOptions)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- # stree-ignore
- t1.as_lines_when_rendering_to_lines(
- collection_bookend: :open
- ) do |t2|
- t2.add_text "#"
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/active_support/operation_tree_builders/hash_with_indifferent_access.rb b/lib/super_diff/active_support/operation_tree_builders/hash_with_indifferent_access.rb
index 35e1d321..8231e5f1 100644
--- a/lib/super_diff/active_support/operation_tree_builders/hash_with_indifferent_access.rb
+++ b/lib/super_diff/active_support/operation_tree_builders/hash_with_indifferent_access.rb
@@ -1,7 +1,7 @@
module SuperDiff
module ActiveSupport
module OperationTreeBuilders
- class HashWithIndifferentAccess < SuperDiff::OperationTreeBuilders::Hash
+ class HashWithIndifferentAccess < Basic::OperationTreeBuilders::Hash
def self.applies_to?(expected, actual)
(
expected.is_a?(::HashWithIndifferentAccess) && actual.is_a?(::Hash)
diff --git a/lib/super_diff/active_support/operation_tree_flatteners/hash_with_indifferent_access.rb b/lib/super_diff/active_support/operation_tree_flatteners/hash_with_indifferent_access.rb
index 2c7c0727..685968d4 100644
--- a/lib/super_diff/active_support/operation_tree_flatteners/hash_with_indifferent_access.rb
+++ b/lib/super_diff/active_support/operation_tree_flatteners/hash_with_indifferent_access.rb
@@ -1,7 +1,7 @@
module SuperDiff
module ActiveSupport
module OperationTreeFlatteners
- class HashWithIndifferentAccess < SuperDiff::OperationTreeFlatteners::Hash
+ class HashWithIndifferentAccess < Basic::OperationTreeFlatteners::Hash
protected
def open_token
diff --git a/lib/super_diff/active_support/operation_trees/hash_with_indifferent_access.rb b/lib/super_diff/active_support/operation_trees/hash_with_indifferent_access.rb
index e06608e4..39e0c4f7 100644
--- a/lib/super_diff/active_support/operation_trees/hash_with_indifferent_access.rb
+++ b/lib/super_diff/active_support/operation_trees/hash_with_indifferent_access.rb
@@ -1,7 +1,7 @@
module SuperDiff
module ActiveSupport
module OperationTrees
- class HashWithIndifferentAccess < SuperDiff::OperationTrees::Base
+ class HashWithIndifferentAccess < Core::AbstractOperationTree
protected
def operation_tree_flattener_class
diff --git a/lib/super_diff/basic.rb b/lib/super_diff/basic.rb
new file mode 100644
index 00000000..82c9dd14
--- /dev/null
+++ b/lib/super_diff/basic.rb
@@ -0,0 +1,48 @@
+require "super_diff/basic/differs"
+require "super_diff/basic/inspection_tree_builders"
+require "super_diff/basic/operation_tree_builders"
+require "super_diff/basic/operation_tree_flatteners"
+require "super_diff/basic/operation_trees"
+
+module SuperDiff
+ module Basic
+ autoload :DiffFormatters, "super_diff/basic/diff_formatters"
+
+ SuperDiff.configuration.tap do |config|
+ config.add_extra_differ_classes(
+ Differs::Array,
+ Differs::Hash,
+ Differs::TimeLike,
+ Differs::DateLike,
+ Differs::MultilineString,
+ Differs::CustomObject,
+ Differs::DefaultObject
+ )
+
+ config.add_extra_inspection_tree_builder_classes(
+ InspectionTreeBuilders::CustomObject,
+ InspectionTreeBuilders::Array,
+ InspectionTreeBuilders::Hash,
+ InspectionTreeBuilders::Primitive,
+ InspectionTreeBuilders::TimeLike,
+ InspectionTreeBuilders::DateLike,
+ InspectionTreeBuilders::DefaultObject
+ )
+
+ config.add_extra_operation_tree_builder_classes(
+ OperationTreeBuilders::Array,
+ OperationTreeBuilders::Hash,
+ OperationTreeBuilders::TimeLike,
+ OperationTreeBuilders::DateLike,
+ OperationTreeBuilders::CustomObject
+ )
+
+ config.add_extra_operation_tree_classes(
+ OperationTrees::Array,
+ OperationTrees::Hash,
+ OperationTrees::CustomObject,
+ OperationTrees::DefaultObject
+ )
+ end
+ end
+end
diff --git a/lib/super_diff/basic/diff_formatters.rb b/lib/super_diff/basic/diff_formatters.rb
new file mode 100644
index 00000000..cac4893d
--- /dev/null
+++ b/lib/super_diff/basic/diff_formatters.rb
@@ -0,0 +1,11 @@
+module SuperDiff
+ module Basic
+ module DiffFormatters
+ autoload :Collection, "super_diff/basic/diff_formatters/collection"
+ autoload(
+ :MultilineString,
+ "super_diff/basic/diff_formatters/multiline_string"
+ )
+ end
+ end
+end
diff --git a/lib/super_diff/basic/diff_formatters/collection.rb b/lib/super_diff/basic/diff_formatters/collection.rb
new file mode 100644
index 00000000..5cfbe31d
--- /dev/null
+++ b/lib/super_diff/basic/diff_formatters/collection.rb
@@ -0,0 +1,135 @@
+module SuperDiff
+ module Basic
+ module DiffFormatters
+ # TODO: Remove
+ class Collection
+ extend AttrExtras.mixin
+
+ ICONS = { delete: "-", insert: "+" }.freeze
+ STYLES = { insert: :actual, delete: :expected, noop: :plain }.freeze
+
+ method_object(
+ %i[
+ open_token!
+ close_token!
+ operation_tree!
+ indent_level!
+ add_comma!
+ collection_prefix!
+ build_item_prefix!
+ ]
+ )
+
+ def call
+ lines.join("\n")
+ end
+
+ private
+
+ attr_query :add_comma?
+
+ def lines
+ [
+ " #{indentation}#{collection_prefix}#{open_token}",
+ *contents,
+ " #{indentation}#{close_token}#{comma}"
+ ]
+ end
+
+ def contents
+ operation_tree.map do |operation|
+ if operation.name == :change
+ handle_change_operation(operation)
+ else
+ handle_non_change_operation(operation)
+ end
+ end
+ end
+
+ def handle_change_operation(operation)
+ SuperDiff::RecursionGuard.guarding_recursion_of(
+ operation.left_collection,
+ operation.right_collection
+ ) do |already_seen|
+ if already_seen
+ raise "Infinite recursion!"
+ else
+ operation.child_operations.to_diff(
+ indent_level: indent_level + 1,
+ collection_prefix: build_item_prefix.call(operation),
+ add_comma: operation.should_add_comma_after_displaying?
+ )
+ end
+ end
+ end
+
+ def handle_non_change_operation(operation)
+ icon = ICONS.fetch(operation.name, " ")
+ style_name = STYLES.fetch(operation.name, :normal)
+ chunk =
+ build_chunk_for(
+ operation,
+ prefix: build_item_prefix.call(operation),
+ icon: icon
+ )
+
+ chunk << "," if operation.should_add_comma_after_displaying?
+
+ style_chunk(style_name, chunk)
+ end
+
+ def build_chunk_for(operation, prefix:, icon:)
+ if operation.value.equal?(operation.collection)
+ build_chunk_from_string(
+ SuperDiff::RecursionGuard::PLACEHOLDER,
+ prefix: build_item_prefix.call(operation),
+ icon: icon
+ )
+ else
+ build_chunk_by_inspecting(
+ operation.value,
+ prefix: build_item_prefix.call(operation),
+ icon: icon
+ )
+ end
+ end
+
+ def build_chunk_by_inspecting(value, prefix:, icon:)
+ inspection = SuperDiff.inspect_object(value, as_single_line: false)
+ build_chunk_from_string(inspection, prefix: prefix, icon: icon)
+ end
+
+ def build_chunk_from_string(value, prefix:, icon:)
+ value
+ .split("\n")
+ .map
+ .with_index do |line, index|
+ [
+ icon,
+ " ",
+ indentation(offset: 1),
+ (index == 0 ? prefix : ""),
+ line
+ ].join
+ end
+ .join("\n")
+ end
+
+ def style_chunk(style_name, chunk)
+ chunk
+ .split("\n")
+ .map { |line| Helpers.style(style_name, line) }
+ .join("\n")
+ end
+
+ def indentation(offset: 0)
+ " " * (indent_level + offset)
+ end
+
+ def comma
+ add_comma? ? "," : ""
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/diff_formatters/multiline_string.rb b/lib/super_diff/basic/diff_formatters/multiline_string.rb
new file mode 100644
index 00000000..a5a58dfb
--- /dev/null
+++ b/lib/super_diff/basic/diff_formatters/multiline_string.rb
@@ -0,0 +1,34 @@
+module SuperDiff
+ module Core
+ module DiffFormatters
+ # TODO: Remove
+ class MultilineString < Base
+ def self.applies_to?(operation_tree)
+ operation_tree.is_a?(OperationTrees::MultilineString)
+ end
+
+ def call
+ lines.join("\n")
+ end
+
+ private
+
+ def lines
+ operation_tree.reduce([]) do |array, operation|
+ case operation.name
+ when :change
+ array << Helpers.style(:expected, "- #{operation.left_value}")
+ array << Helpers.style(:actual, "+ #{operation.right_value}")
+ when :delete
+ array << Helpers.style(:expected, "- #{operation.value}")
+ when :insert
+ array << Helpers.style(:actual, "+ #{operation.value}")
+ else
+ array << Helpers.style(:plain, " #{operation.value}")
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/differs.rb b/lib/super_diff/basic/differs.rb
new file mode 100644
index 00000000..1e154929
--- /dev/null
+++ b/lib/super_diff/basic/differs.rb
@@ -0,0 +1,24 @@
+module SuperDiff
+ module Basic
+ module Differs
+ autoload :Array, "super_diff/basic/differs/array"
+ autoload :CustomObject, "super_diff/basic/differs/custom_object"
+ autoload :DateLike, "super_diff/basic/differs/date_like"
+ autoload :DefaultObject, "super_diff/basic/differs/default_object"
+ autoload :Hash, "super_diff/basic/differs/hash"
+ autoload :MultilineString, "super_diff/basic/differs/multiline_string"
+ autoload :TimeLike, "super_diff/basic/differs/time_like"
+
+ class Main
+ def self.call(*args)
+ warn <<~EOT
+ WARNING: SuperDiff::Differs::Main.call(...) is deprecated and will be removed in the next major release.
+ Please use SuperDiff.diff(...) instead.
+ #{caller_locations.join("\n")}
+ EOT
+ SuperDiff.diff(*args)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/differs/array.rb b/lib/super_diff/basic/differs/array.rb
new file mode 100644
index 00000000..601059b3
--- /dev/null
+++ b/lib/super_diff/basic/differs/array.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module Differs
+ class Array < Core::AbstractDiffer
+ def self.applies_to?(expected, actual)
+ expected.is_a?(::Array) && actual.is_a?(::Array)
+ end
+
+ protected
+
+ def operation_tree_builder_class
+ OperationTreeBuilders::Array
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/differs/custom_object.rb b/lib/super_diff/basic/differs/custom_object.rb
new file mode 100644
index 00000000..47ff00fb
--- /dev/null
+++ b/lib/super_diff/basic/differs/custom_object.rb
@@ -0,0 +1,19 @@
+module SuperDiff
+ module Basic
+ module Differs
+ class CustomObject < Core::AbstractDiffer
+ def self.applies_to?(expected, actual)
+ expected.class == actual.class &&
+ expected.respond_to?(:attributes_for_super_diff) &&
+ actual.respond_to?(:attributes_for_super_diff)
+ end
+
+ protected
+
+ def operation_tree_builder_class
+ OperationTreeBuilders::CustomObject
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/differs/date_like.rb b/lib/super_diff/basic/differs/date_like.rb
new file mode 100644
index 00000000..22219b60
--- /dev/null
+++ b/lib/super_diff/basic/differs/date_like.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module Differs
+ class DateLike < Core::AbstractDiffer
+ def self.applies_to?(expected, actual)
+ SuperDiff.date_like?(expected) && SuperDiff.date_like?(actual)
+ end
+
+ protected
+
+ def operation_tree_builder_class
+ OperationTreeBuilders::DateLike
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/differs/default_object.rb b/lib/super_diff/basic/differs/default_object.rb
new file mode 100644
index 00000000..0b0b7307
--- /dev/null
+++ b/lib/super_diff/basic/differs/default_object.rb
@@ -0,0 +1,24 @@
+module SuperDiff
+ module Basic
+ module Differs
+ class DefaultObject < Core::AbstractDiffer
+ def self.applies_to?(expected, actual)
+ expected.class == actual.class
+ end
+
+ protected
+
+ def operation_tree
+ SuperDiff.build_operation_tree_for(
+ expected,
+ actual,
+ extra_operation_tree_builder_classes: [
+ SuperDiff::Basic::OperationTreeBuilders::DefaultObject
+ ],
+ raise_if_nothing_applies: true
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/differs/hash.rb b/lib/super_diff/basic/differs/hash.rb
new file mode 100644
index 00000000..17834446
--- /dev/null
+++ b/lib/super_diff/basic/differs/hash.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module Differs
+ class Hash < Core::AbstractDiffer
+ def self.applies_to?(expected, actual)
+ expected.is_a?(::Hash) && actual.is_a?(::Hash)
+ end
+
+ protected
+
+ def operation_tree_builder_class
+ OperationTreeBuilders::Hash
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/differs/multiline_string.rb b/lib/super_diff/basic/differs/multiline_string.rb
new file mode 100644
index 00000000..72fdf12e
--- /dev/null
+++ b/lib/super_diff/basic/differs/multiline_string.rb
@@ -0,0 +1,18 @@
+module SuperDiff
+ module Basic
+ module Differs
+ class MultilineString < Core::AbstractDiffer
+ def self.applies_to?(expected, actual)
+ expected.is_a?(::String) && actual.is_a?(::String) &&
+ (expected.include?("\n") || actual.include?("\n"))
+ end
+
+ protected
+
+ def operation_tree_builder_class
+ OperationTreeBuilders::MultilineString
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/differs/time_like.rb b/lib/super_diff/basic/differs/time_like.rb
new file mode 100644
index 00000000..9837e350
--- /dev/null
+++ b/lib/super_diff/basic/differs/time_like.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module Differs
+ class TimeLike < Core::AbstractDiffer
+ def self.applies_to?(expected, actual)
+ SuperDiff.time_like?(expected) && SuperDiff.time_like?(actual)
+ end
+
+ protected
+
+ def operation_tree_builder_class
+ OperationTreeBuilders::TimeLike
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/inspection_tree_builders.rb b/lib/super_diff/basic/inspection_tree_builders.rb
new file mode 100644
index 00000000..e7101e1b
--- /dev/null
+++ b/lib/super_diff/basic/inspection_tree_builders.rb
@@ -0,0 +1,20 @@
+module SuperDiff
+ module Basic
+ module InspectionTreeBuilders
+ autoload :Array, "super_diff/basic/inspection_tree_builders/array"
+ autoload(
+ :CustomObject,
+ "super_diff/basic/inspection_tree_builders/custom_object"
+ )
+ autoload(
+ :DefaultObject,
+ "super_diff/basic/inspection_tree_builders/default_object"
+ )
+ autoload :Hash, "super_diff/basic/inspection_tree_builders/hash"
+ autoload :Primitive, "super_diff/basic/inspection_tree_builders/primitive"
+ autoload :String, "super_diff/basic/inspection_tree_builders/string"
+ autoload :TimeLike, "super_diff/basic/inspection_tree_builders/time_like"
+ autoload :DateLike, "super_diff/basic/inspection_tree_builders/date_like"
+ end
+ end
+end
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/array.rb b/lib/super_diff/basic/inspection_tree_builders/array.rb
similarity index 90%
rename from lib/super_diff/object_inspection/inspection_tree_builders/array.rb
rename to lib/super_diff/basic/inspection_tree_builders/array.rb
index 1a6bfb2b..f4105cbf 100644
--- a/lib/super_diff/object_inspection/inspection_tree_builders/array.rb
+++ b/lib/super_diff/basic/inspection_tree_builders/array.rb
@@ -1,13 +1,13 @@
module SuperDiff
- module ObjectInspection
+ module Basic
module InspectionTreeBuilders
- class Array < Base
+ class Array < Core::AbstractInspectionTreeBuilder
def self.applies_to?(value)
value.is_a?(::Array)
end
def call
- InspectionTree.new do |t1|
+ Core::InspectionTree.new do |t1|
t1.only_when empty do |t2|
# stree-ignore
t2.as_lines_when_rendering_to_lines do |t3|
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/custom_object.rb b/lib/super_diff/basic/inspection_tree_builders/custom_object.rb
similarity index 88%
rename from lib/super_diff/object_inspection/inspection_tree_builders/custom_object.rb
rename to lib/super_diff/basic/inspection_tree_builders/custom_object.rb
index 741db24a..92016ae4 100644
--- a/lib/super_diff/object_inspection/inspection_tree_builders/custom_object.rb
+++ b/lib/super_diff/basic/inspection_tree_builders/custom_object.rb
@@ -1,13 +1,13 @@
module SuperDiff
- module ObjectInspection
+ module Basic
module InspectionTreeBuilders
- class CustomObject < Base
+ class CustomObject < Core::AbstractInspectionTreeBuilder
def self.applies_to?(value)
value.respond_to?(:attributes_for_super_diff)
end
def call
- InspectionTree.new do |t1|
+ Core::InspectionTree.new do |t1|
t1.as_lines_when_rendering_to_lines(
collection_bookend: :open
) do |t2|
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/date_like.rb b/lib/super_diff/basic/inspection_tree_builders/date_like.rb
similarity index 91%
rename from lib/super_diff/object_inspection/inspection_tree_builders/date_like.rb
rename to lib/super_diff/basic/inspection_tree_builders/date_like.rb
index a8dc6402..ced3ef24 100644
--- a/lib/super_diff/object_inspection/inspection_tree_builders/date_like.rb
+++ b/lib/super_diff/basic/inspection_tree_builders/date_like.rb
@@ -1,13 +1,13 @@
module SuperDiff
- module ObjectInspection
+ module Basic
module InspectionTreeBuilders
- class DateLike < Base
+ class DateLike < Core::AbstractInspectionTreeBuilder
def self.applies_to?(value)
SuperDiff.date_like?(value)
end
def call
- InspectionTree.new do |t1|
+ Core::InspectionTree.new do |t1|
t1.as_lines_when_rendering_to_lines(
collection_bookend: :open
) do |t2|
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/default_object.rb b/lib/super_diff/basic/inspection_tree_builders/default_object.rb
similarity index 84%
rename from lib/super_diff/object_inspection/inspection_tree_builders/default_object.rb
rename to lib/super_diff/basic/inspection_tree_builders/default_object.rb
index 42a54d58..c479224b 100644
--- a/lib/super_diff/object_inspection/inspection_tree_builders/default_object.rb
+++ b/lib/super_diff/basic/inspection_tree_builders/default_object.rb
@@ -1,18 +1,17 @@
module SuperDiff
- module ObjectInspection
+ module Basic
module InspectionTreeBuilders
- class DefaultObject < Base
+ class DefaultObject < Core::AbstractInspectionTreeBuilder
def self.applies_to?(_value)
true
end
def call
- InspectionTree.new do |t1|
+ Core::InspectionTree.new do |t1|
t1.only_when empty do |t2|
t2.as_lines_when_rendering_to_lines do |t3|
t3.add_text(
- "#<#{object.class.name}:" +
- SuperDiff::Helpers.object_address_for(object) + ">"
+ "#<#{object.class.name}:" + object_address_for(object) + ">"
)
end
end
@@ -22,8 +21,7 @@ def call
collection_bookend: :open
) do |t3|
t3.add_text(
- "#<#{object.class.name}:" +
- SuperDiff::Helpers.object_address_for(object)
+ "#<#{object.class.name}:" + object_address_for(object)
)
# stree-ignore
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/hash.rb b/lib/super_diff/basic/inspection_tree_builders/hash.rb
similarity index 92%
rename from lib/super_diff/object_inspection/inspection_tree_builders/hash.rb
rename to lib/super_diff/basic/inspection_tree_builders/hash.rb
index ecce40bd..f784b51b 100644
--- a/lib/super_diff/object_inspection/inspection_tree_builders/hash.rb
+++ b/lib/super_diff/basic/inspection_tree_builders/hash.rb
@@ -1,13 +1,13 @@
module SuperDiff
- module ObjectInspection
+ module Basic
module InspectionTreeBuilders
- class Hash < Base
+ class Hash < Core::AbstractInspectionTreeBuilder
def self.applies_to?(value)
value.is_a?(::Hash)
end
def call
- InspectionTree.new do |t1|
+ Core::InspectionTree.new do |t1|
t1.only_when empty do |t2|
# stree-ignore
t2.as_lines_when_rendering_to_lines do |t3|
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/primitive.rb b/lib/super_diff/basic/inspection_tree_builders/primitive.rb
similarity index 74%
rename from lib/super_diff/object_inspection/inspection_tree_builders/primitive.rb
rename to lib/super_diff/basic/inspection_tree_builders/primitive.rb
index 4a3b021e..a1c4602f 100644
--- a/lib/super_diff/object_inspection/inspection_tree_builders/primitive.rb
+++ b/lib/super_diff/basic/inspection_tree_builders/primitive.rb
@@ -1,13 +1,13 @@
module SuperDiff
- module ObjectInspection
+ module Basic
module InspectionTreeBuilders
- class Primitive < Base
+ class Primitive < Core::AbstractInspectionTreeBuilder
def self.applies_to?(value)
SuperDiff.primitive?(value) || value.is_a?(::String)
end
def call
- InspectionTree.new do |t1|
+ Core::InspectionTree.new do |t1|
t1.as_lines_when_rendering_to_lines do |t2|
t2.add_text object.inspect
end
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/time_like.rb b/lib/super_diff/basic/inspection_tree_builders/time_like.rb
similarity index 93%
rename from lib/super_diff/object_inspection/inspection_tree_builders/time_like.rb
rename to lib/super_diff/basic/inspection_tree_builders/time_like.rb
index 0739a2d6..61908030 100644
--- a/lib/super_diff/object_inspection/inspection_tree_builders/time_like.rb
+++ b/lib/super_diff/basic/inspection_tree_builders/time_like.rb
@@ -1,13 +1,13 @@
module SuperDiff
- module ObjectInspection
+ module Basic
module InspectionTreeBuilders
- class TimeLike < Base
+ class TimeLike < Core::AbstractInspectionTreeBuilder
def self.applies_to?(value)
SuperDiff.time_like?(value)
end
def call
- InspectionTree.new do |t1|
+ Core::InspectionTree.new do |t1|
t1.as_lines_when_rendering_to_lines(
collection_bookend: :open
) do |t2|
diff --git a/lib/super_diff/basic/operation_tree_builders.rb b/lib/super_diff/basic/operation_tree_builders.rb
new file mode 100644
index 00000000..a5bf8d03
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_builders.rb
@@ -0,0 +1,34 @@
+module SuperDiff
+ module Basic
+ module OperationTreeBuilders
+ autoload :Array, "super_diff/basic/operation_tree_builders/array"
+ autoload(
+ :CustomObject,
+ "super_diff/basic/operation_tree_builders/custom_object"
+ )
+ autoload(
+ :DefaultObject,
+ "super_diff/basic/operation_tree_builders/default_object"
+ )
+ autoload :Hash, "super_diff/basic/operation_tree_builders/hash"
+ # TODO: Where is this used?
+ autoload(
+ :MultilineString,
+ "super_diff/basic/operation_tree_builders/multiline_string"
+ )
+ autoload :TimeLike, "super_diff/basic/operation_tree_builders/time_like"
+ autoload :DateLike, "super_diff/basic/operation_tree_builders/date_like"
+
+ class Main
+ def self.call(*args)
+ warn <<~EOT
+ WARNING: SuperDiff::OperationTreeBuilders::Main.call(...) is deprecated and will be removed in the next major release.
+ Please use SuperDiff.build_operation_tree_for(...) instead.
+ #{caller_locations.join("\n")}
+ EOT
+ SuperDiff.build_operation_tree_for(*args)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_builders/array.rb b/lib/super_diff/basic/operation_tree_builders/array.rb
new file mode 100644
index 00000000..8fa4e43b
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_builders/array.rb
@@ -0,0 +1,111 @@
+require "diff-lcs"
+
+module SuperDiff
+ module Basic
+ module OperationTreeBuilders
+ class Array < Core::AbstractOperationTreeBuilder
+ def self.applies_to?(expected, actual)
+ expected.is_a?(::Array) && actual.is_a?(::Array)
+ end
+
+ def call
+ Diff::LCS.traverse_balanced(expected, actual, lcs_callbacks)
+ operation_tree
+ end
+
+ private
+
+ def lcs_callbacks
+ @_lcs_callbacks ||=
+ LcsCallbacks.new(
+ operation_tree: operation_tree,
+ expected: expected,
+ actual: actual,
+ compare: method(:compare)
+ )
+ end
+
+ def operation_tree
+ @_operation_tree ||= OperationTrees::Array.new([])
+ end
+
+ class LcsCallbacks
+ extend AttrExtras.mixin
+
+ pattr_initialize %i[operation_tree! expected! actual! compare!]
+ public :operation_tree
+
+ def match(event)
+ add_noop_operation(event)
+ end
+
+ def discard_a(event)
+ add_delete_operation(event)
+ end
+
+ def discard_b(event)
+ add_insert_operation(event)
+ end
+
+ def change(event)
+ children = compare.(event.old_element, event.new_element)
+
+ if children
+ add_change_operation(event, children)
+ else
+ add_delete_operation(event)
+ add_insert_operation(event)
+ end
+ end
+
+ private
+
+ def add_delete_operation(event)
+ operation_tree << Core::UnaryOperation.new(
+ name: :delete,
+ collection: expected,
+ key: event.old_position,
+ value: event.old_element,
+ index: event.old_position
+ )
+ end
+
+ def add_insert_operation(event)
+ operation_tree << Core::UnaryOperation.new(
+ name: :insert,
+ collection: actual,
+ key: event.new_position,
+ value: event.new_element,
+ index: event.new_position
+ )
+ end
+
+ def add_noop_operation(event)
+ operation_tree << Core::UnaryOperation.new(
+ name: :noop,
+ collection: actual,
+ key: event.new_position,
+ value: event.new_element,
+ index: event.new_position
+ )
+ end
+
+ def add_change_operation(event, children)
+ operation_tree << Core::BinaryOperation.new(
+ name: :change,
+ left_collection: expected,
+ right_collection: actual,
+ left_key: event.old_position,
+ right_key: event.new_position,
+ left_value: event.old_element,
+ right_value: event.new_element,
+ left_index: event.old_position,
+ right_index: event.new_position,
+ children: children
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_builders/custom_object.rb b/lib/super_diff/basic/operation_tree_builders/custom_object.rb
new file mode 100644
index 00000000..14f0c543
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_builders/custom_object.rb
@@ -0,0 +1,42 @@
+module SuperDiff
+ module Basic
+ module OperationTreeBuilders
+ class CustomObject < DefaultObject
+ def self.applies_to?(expected, actual)
+ expected.class == actual.class &&
+ expected.respond_to?(:attributes_for_super_diff) &&
+ actual.respond_to?(:attributes_for_super_diff)
+ end
+
+ protected
+
+ def build_operation_tree
+ # NOTE: It doesn't matter whether we use expected or actual here,
+ # because all we care about is the name of the class
+ OperationTrees::CustomObject.new([], underlying_object: actual)
+ end
+
+ def attribute_names
+ expected.attributes_for_super_diff.keys &
+ actual.attributes_for_super_diff.keys
+ end
+
+ private
+
+ attr_reader :expected_attributes, :actual_attributes
+
+ def establish_expected_and_actual_attributes
+ @expected_attributes =
+ attribute_names.reduce({}) do |hash, name|
+ hash.merge(name => expected.public_send(name))
+ end
+
+ @actual_attributes =
+ attribute_names.reduce({}) do |hash, name|
+ hash.merge(name => actual.public_send(name))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_builders/date_like.rb b/lib/super_diff/basic/operation_tree_builders/date_like.rb
new file mode 100644
index 00000000..2f8a9b4a
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_builders/date_like.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module OperationTreeBuilders
+ class DateLike < CustomObject
+ def self.applies_to?(expected, actual)
+ SuperDiff.date_like?(expected) && SuperDiff.date_like?(actual)
+ end
+
+ protected
+
+ def attribute_names
+ %w[year month day]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_builders/default_object.rb b/lib/super_diff/basic/operation_tree_builders/default_object.rb
new file mode 100644
index 00000000..f1ecbf4b
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_builders/default_object.rb
@@ -0,0 +1,117 @@
+module SuperDiff
+ module Basic
+ module OperationTreeBuilders
+ class DefaultObject < Core::AbstractOperationTreeBuilder
+ def self.applies_to?(_expected, _actual)
+ true
+ end
+
+ def initialize(*args)
+ super(*args)
+
+ establish_expected_and_actual_attributes
+ end
+
+ protected
+
+ def unary_operations
+ attribute_names.reduce([]) do |operations, name|
+ possibly_add_noop_operation_to(operations, name)
+ possibly_add_delete_operation_to(operations, name)
+ possibly_add_insert_operation_to(operations, name)
+ operations
+ end
+ end
+
+ def build_operation_tree
+ # XXX This assumes that `expected` and `actual` are the same
+ # TODO: Does this need to find the operation tree matching `actual`?
+ OperationTrees::DefaultObject.new([], underlying_object: actual)
+ end
+
+ def attribute_names
+ (
+ expected.instance_variables.sort & actual.instance_variables.sort
+ ).map { |variable_name| variable_name[1..-1] }
+ end
+
+ private
+
+ attr_reader :expected_attributes, :actual_attributes
+
+ def establish_expected_and_actual_attributes
+ @expected_attributes =
+ attribute_names.reduce({}) do |hash, name|
+ hash.merge(name => expected.instance_variable_get("@#{name}"))
+ end
+
+ @actual_attributes =
+ attribute_names.reduce({}) do |hash, name|
+ hash.merge(name => actual.instance_variable_get("@#{name}"))
+ end
+ end
+
+ def possibly_add_noop_operation_to(operations, attribute_name)
+ if should_add_noop_operation?(attribute_name)
+ operations << Core::UnaryOperation.new(
+ name: :noop,
+ collection: actual_attributes,
+ key: attribute_name,
+ index: attribute_names.index(attribute_name),
+ value: actual_attributes[attribute_name]
+ )
+ end
+ end
+
+ def should_add_noop_operation?(attribute_name)
+ expected_attributes.include?(attribute_name) &&
+ actual_attributes.include?(attribute_name) &&
+ expected_attributes[attribute_name] ==
+ actual_attributes[attribute_name]
+ end
+
+ def possibly_add_delete_operation_to(operations, attribute_name)
+ if should_add_delete_operation?(attribute_name)
+ operations << Core::UnaryOperation.new(
+ name: :delete,
+ collection: expected_attributes,
+ key: attribute_name,
+ index: attribute_names.index(attribute_name),
+ value: expected_attributes[attribute_name]
+ )
+ end
+ end
+
+ def should_add_delete_operation?(attribute_name)
+ expected_attributes.include?(attribute_name) &&
+ (
+ !actual_attributes.include?(attribute_name) ||
+ expected_attributes[attribute_name] !=
+ actual_attributes[attribute_name]
+ )
+ end
+
+ def possibly_add_insert_operation_to(operations, attribute_name)
+ if should_add_insert_operation?(attribute_name)
+ operations << Core::UnaryOperation.new(
+ name: :insert,
+ collection: actual_attributes,
+ key: attribute_name,
+ index: attribute_names.index(attribute_name),
+ value: actual_attributes[attribute_name]
+ )
+ end
+ end
+
+ def should_add_insert_operation?(attribute_name)
+ !expected_attributes.include?(attribute_name) ||
+ (
+ actual_attributes.include?(attribute_name) &&
+ expected_attributes[attribute_name] !=
+ actual_attributes[attribute_name]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_builders/hash.rb b/lib/super_diff/basic/operation_tree_builders/hash.rb
new file mode 100644
index 00000000..df09ceda
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_builders/hash.rb
@@ -0,0 +1,222 @@
+module SuperDiff
+ module Basic
+ module OperationTreeBuilders
+ class Hash < Core::AbstractOperationTreeBuilder
+ def self.applies_to?(expected, actual)
+ expected.is_a?(::Hash) && actual.is_a?(::Hash)
+ end
+
+ protected
+
+ def unary_operations
+ unary_operations_using_variant_of_patience_algorithm
+ end
+
+ def build_operation_tree
+ OperationTrees::Hash.new([])
+ end
+
+ private
+
+ def unary_operations_using_variant_of_patience_algorithm
+ operations = []
+ aks, eks = actual.keys, expected.keys
+ previous_ei, ei = nil, 0
+ ai = 0
+
+ # When diffing a hash, we're more interested in the 'actual' version
+ # than the 'expected' version, because that's the ultimate truth.
+ # Therefore, the diff is presented from the perspective of the 'actual'
+ # hash, and we start off by looping over it.
+ while ai < aks.size
+ ak = aks[ai]
+ av, ev = actual[ak], expected[ak]
+ # While we iterate over 'actual' in order, we jump all over
+ # 'expected', trying to match up its keys with the keys in 'actual' as
+ # much as possible.
+ ei = eks.index(ak)
+
+ if should_add_noop_operation?(ak)
+ # (If we're here, it probably means that the key we're pointing to
+ # in the 'actual' and 'expected' hashes have the same value.)
+
+ if ei && previous_ei && (ei - previous_ei) > 1
+ # If we've jumped from one operation in the 'expected' hash to
+ # another operation later in 'expected' (due to the fact that the
+ # 'expected' hash is in a different order than 'actual'), collect
+ # any delete operations in between and add them to our operations
+ # array as deletes before adding the noop. If we don't do this
+ # now, then those deletes will disappear. (Again, we are mainly
+ # iterating over 'actual', so this is the only way to catch all of
+ # the keys in 'expected'.)
+ (previous_ei + 1).upto(ei - 1) do |ei2|
+ ek = eks[ei2]
+ ev2, av2 = expected[ek], actual[ek]
+
+ if (
+ (!actual.include?(ek) || ev2 != av2) &&
+ operations.none? do |operation|
+ %i[delete noop].include?(operation.name) &&
+ operation.key == ek
+ end
+ )
+ operations << Core::UnaryOperation.new(
+ name: :delete,
+ collection: expected,
+ key: ek,
+ value: ev2,
+ index: ei2
+ )
+ end
+ end
+ end
+
+ operations << Core::UnaryOperation.new(
+ name: :noop,
+ collection: actual,
+ key: ak,
+ value: av,
+ index: ai
+ )
+ else
+ # (If we're here, it probably means that the key in 'actual' isn't
+ # present in 'expected' or the values don't match.)
+
+ if (
+ (operations.empty? || operations.last.name == :noop) &&
+ (ai == 0 || eks.include?(aks[ai - 1]))
+ )
+ # If we go from a match in the last iteration to a missing or
+ # extra key in this one, or we're at the first key in 'actual' and
+ # it's missing or extra, look for deletes in the 'expected' hash
+ # and add them to our list of operations before we add the
+ # inserts. In most cases we will accomplish this by backtracking a
+ # bit to the key in 'expected' that matched the key in 'actual' we
+ # processed in the previous iteration (or just the first key in
+ # 'expected' if this is the first key in 'actual'), and then
+ # iterating from there through 'expected' until we reach the end
+ # or we hit some other condition (see below).
+
+ start_index =
+ if ai > 0
+ eks.index(aks[ai - 1]) + 1
+ else
+ 0
+ end
+
+ start_index.upto(eks.size - 1) do |ei2|
+ ek = eks[ei2]
+ ev, av2 = expected[ek], actual[ek]
+
+ if actual.include?(ek) && ev == av2
+ # If the key in 'expected' we've landed on happens to be a
+ # match in 'actual', then stop, because it's going to be
+ # handled in some future iteration of the 'actual' loop.
+ break
+ elsif (
+ aks[ai + 1..-1].any? do |k|
+ expected.include?(k) && expected[k] != actual[k]
+ end
+ )
+ # While we backtracked a bit to iterate over 'expected', we
+ # now have to look ahead. If we will end up encountering a
+ # insert that matches this delete later, stop and go back to
+ # iterating over 'actual'. This is because the delete we would
+ # have added now will be added later when we encounter the
+ # associated insert, so we don't want to add it twice.
+ break
+ else
+ operations << Core::UnaryOperation.new(
+ name: :delete,
+ collection: expected,
+ key: ek,
+ value: ev,
+ index: ei2
+ )
+ end
+
+ if ek == ak && ev != av
+ # If we're pointing to the same key in 'expected' as in
+ # 'actual', but with different values, go ahead and add an
+ # insert now to accompany the delete added above. That way
+ # they appear together, which will be easier to read.
+ operations << Core::UnaryOperation.new(
+ name: :insert,
+ collection: actual,
+ key: ak,
+ value: av,
+ index: ai
+ )
+ end
+ end
+ end
+
+ if (
+ expected.include?(ak) && ev != av &&
+ operations.none? do |op|
+ op.name == :delete && op.key == ak
+ end
+ )
+ # If we're here, it means that we didn't encounter any delete
+ # operations above for whatever reason and so we need to add a
+ # delete to represent the fact that the value for this key has
+ # changed.
+ operations << Core::UnaryOperation.new(
+ name: :delete,
+ collection: expected,
+ key: ak,
+ value: expected[ak],
+ index: ei
+ )
+ end
+
+ if operations.none? { |op| op.name == :insert && op.key == ak }
+ # If we're here, it means that we didn't encounter any insert
+ # operations above. Since we already handled delete, the only
+ # alternative is that this key must not exist in 'expected', so
+ # we need to add an insert.
+ operations << Core::UnaryOperation.new(
+ name: :insert,
+ collection: actual,
+ key: ak,
+ value: av,
+ index: ai
+ )
+ end
+ end
+
+ ai += 1
+ previous_ei = ei
+ end
+
+ # The last thing to do is this: if there are keys in 'expected' that
+ # aren't in 'actual', and they aren't associated with any inserts to
+ # where they would have been added above, tack those deletes onto the
+ # end of our operations array.
+ (eks - aks - operations.map(&:key)).each do |ek|
+ ei = eks.index(ek)
+ ev = expected[ek]
+
+ operations << Core::UnaryOperation.new(
+ name: :delete,
+ collection: expected,
+ key: ek,
+ value: ev,
+ index: ei
+ )
+ end
+
+ operations
+ end
+
+ def should_add_noop_operation?(key)
+ expected.include?(key) && expected[key] == actual[key]
+ end
+
+ def all_keys
+ actual.keys | expected.keys
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_builders/multiline_string.rb b/lib/super_diff/basic/operation_tree_builders/multiline_string.rb
new file mode 100644
index 00000000..d6f1d9e2
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_builders/multiline_string.rb
@@ -0,0 +1,90 @@
+require "patience_diff"
+
+module SuperDiff
+ module Basic
+ module OperationTreeBuilders
+ class MultilineString < Core::AbstractOperationTreeBuilder
+ def self.applies_to?(expected, actual)
+ expected.is_a?(::String) && actual.is_a?(::String) &&
+ (expected.include?("\n") || actual.include?("\n"))
+ end
+
+ def initialize(*args)
+ super(*args)
+
+ @original_expected = @expected
+ @original_actual = @actual
+ @expected = split_into_lines(@expected)
+ @actual = split_into_lines(@actual)
+ @sequence_matcher = PatienceDiff::SequenceMatcher.new
+ end
+
+ protected
+
+ def unary_operations
+ opcodes.flat_map do |code, a_start, a_end, b_start, b_end|
+ if code == :delete
+ add_delete_operations(a_start..a_end)
+ elsif code == :insert
+ add_insert_operations(b_start..b_end)
+ else
+ add_noop_operations(b_start..b_end)
+ end
+ end
+ end
+
+ def build_operation_tree
+ OperationTrees::MultilineString.new([])
+ end
+
+ private
+
+ attr_reader :sequence_matcher, :original_expected, :original_actual
+
+ def split_into_lines(string)
+ string.scan(/.+(?:\r|\n|\r\n|\Z)/)
+ end
+
+ def opcodes
+ sequence_matcher.diff_opcodes(expected, actual)
+ end
+
+ def add_delete_operations(indices)
+ indices.map do |index|
+ Core::UnaryOperation.new(
+ name: :delete,
+ collection: expected,
+ key: index,
+ index: index,
+ value: expected[index]
+ )
+ end
+ end
+
+ def add_insert_operations(indices)
+ indices.map do |index|
+ Core::UnaryOperation.new(
+ name: :insert,
+ collection: actual,
+ key: index,
+ index: index,
+ value: actual[index]
+ )
+ end
+ end
+
+ def add_noop_operations(indices)
+ indices.map do |index|
+ Core::UnaryOperation.new(
+ name: :noop,
+ collection: actual,
+ key: index,
+ index: index,
+ value: actual[index]
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_builders/time_like.rb b/lib/super_diff/basic/operation_tree_builders/time_like.rb
new file mode 100644
index 00000000..4a1fcc4b
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_builders/time_like.rb
@@ -0,0 +1,26 @@
+module SuperDiff
+ module Basic
+ module OperationTreeBuilders
+ class TimeLike < CustomObject
+ def self.applies_to?(expected, actual)
+ SuperDiff.time_like?(expected) && SuperDiff.time_like?(actual)
+ end
+
+ protected
+
+ def attribute_names
+ base = %w[year month day hour min sec subsec zone utc_offset]
+
+ # If timezones are different, also show a normalized timestamp at the
+ # end of the diff to help visualize why they are different moments in
+ # time.
+ if actual.zone != expected.zone
+ base + ["utc"]
+ else
+ base
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_flatteners.rb b/lib/super_diff/basic/operation_tree_flatteners.rb
new file mode 100644
index 00000000..e36e8008
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_flatteners.rb
@@ -0,0 +1,24 @@
+module SuperDiff
+ module Basic
+ module OperationTreeFlatteners
+ autoload :Array, "super_diff/basic/operation_tree_flatteners/array"
+ autoload(
+ :Collection,
+ "super_diff/basic/operation_tree_flatteners/collection"
+ )
+ autoload(
+ :CustomObject,
+ "super_diff/basic/operation_tree_flatteners/custom_object"
+ )
+ autoload(
+ :DefaultObject,
+ "super_diff/basic/operation_tree_flatteners/default_object"
+ )
+ autoload :Hash, "super_diff/basic/operation_tree_flatteners/hash"
+ autoload(
+ :MultilineString,
+ "super_diff/basic/operation_tree_flatteners/multiline_string"
+ )
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_flatteners/array.rb b/lib/super_diff/basic/operation_tree_flatteners/array.rb
new file mode 100644
index 00000000..f7791ba2
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_flatteners/array.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module OperationTreeFlatteners
+ class Array < Collection
+ protected
+
+ def open_token
+ "["
+ end
+
+ def close_token
+ "]"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_flatteners/collection.rb b/lib/super_diff/basic/operation_tree_flatteners/collection.rb
new file mode 100644
index 00000000..93a68846
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_flatteners/collection.rb
@@ -0,0 +1,140 @@
+module SuperDiff
+ module Basic
+ module OperationTreeFlatteners
+ class Collection < Core::AbstractOperationTreeFlattener
+ protected
+
+ def build_tiered_lines
+ [
+ Core::Line.new(
+ type: :noop,
+ indentation_level: indentation_level,
+ value: open_token,
+ collection_bookend: :open
+ ),
+ *inner_lines,
+ Core::Line.new(
+ type: :noop,
+ indentation_level: indentation_level,
+ value: close_token,
+ collection_bookend: :close
+ )
+ ]
+ end
+
+ def inner_lines
+ @_inner_lines ||=
+ operation_tree.flat_map do |operation|
+ lines =
+ if operation.name == :change
+ build_lines_for_change_operation(operation)
+ else
+ build_lines_for_non_change_operation(operation)
+ end
+
+ maybe_add_prefix_at_beginning_of_lines(
+ maybe_add_comma_at_end_of_lines(lines, operation),
+ operation
+ )
+ end
+ end
+
+ def maybe_add_prefix_at_beginning_of_lines(lines, operation)
+ if add_prefix_at_beginning_of_lines?(operation)
+ add_prefix_at_beginning_of_lines(lines, operation)
+ else
+ lines
+ end
+ end
+
+ def add_prefix_at_beginning_of_lines?(operation)
+ !!item_prefix_for(operation)
+ end
+
+ def add_prefix_at_beginning_of_lines(lines, operation)
+ [lines[0].prefixed_with(item_prefix_for(operation))] + lines[1..-1]
+ end
+
+ def maybe_add_comma_at_end_of_lines(lines, operation)
+ if last_item_in_collection?(operation)
+ lines
+ else
+ add_comma_at_end_of_lines(lines)
+ end
+ end
+
+ def last_item_in_collection?(operation)
+ if operation.name == :change
+ operation.left_index == operation.left_collection.size - 1 &&
+ operation.right_index == operation.right_collection.size - 1
+ else
+ operation.index == operation.collection.size - 1
+ end
+ end
+
+ def add_comma_at_end_of_lines(lines)
+ lines[0..-2] + [lines[-1].with_comma]
+ end
+
+ def build_lines_for_change_operation(operation)
+ Core::RecursionGuard.guarding_recursion_of(
+ operation.left_collection,
+ operation.right_collection
+ ) do |already_seen|
+ if already_seen
+ raise InfiniteRecursionError
+ else
+ operation.children.flatten(
+ indentation_level: indentation_level + 1
+ )
+ end
+ end
+ end
+
+ def build_lines_for_non_change_operation(operation)
+ indentation_level = @indentation_level + 1
+
+ if recursive_operation?(operation)
+ [
+ Core::Line.new(
+ type: operation.name,
+ indentation_level: indentation_level,
+ value: Core::RecursionGuard::PLACEHOLDER
+ )
+ ]
+ else
+ build_lines_from_inspection_of(
+ operation.value,
+ type: operation.name,
+ indentation_level: indentation_level
+ )
+ end
+ end
+
+ def recursive_operation?(operation)
+ operation.value.equal?(operation.collection) ||
+ Core::RecursionGuard.already_seen?(operation.value)
+ end
+
+ def item_prefix_for(_operation)
+ ""
+ end
+
+ def build_lines_from_inspection_of(value, type:, indentation_level:)
+ SuperDiff.inspect_object(
+ value,
+ as_lines: true,
+ type: type,
+ indentation_level: indentation_level
+ )
+ end
+
+ class InfiniteRecursionError < StandardError
+ def initialize(_message = nil)
+ super("Unhandled recursive data structure encountered!")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_flatteners/custom_object.rb b/lib/super_diff/basic/operation_tree_flatteners/custom_object.rb
new file mode 100644
index 00000000..36dbb74b
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_flatteners/custom_object.rb
@@ -0,0 +1,30 @@
+module SuperDiff
+ module Basic
+ module OperationTreeFlatteners
+ class CustomObject < Collection
+ protected
+
+ def open_token
+ "#<%s {" % { class: operation_tree.underlying_object.class }
+ end
+
+ def close_token
+ "}>"
+ end
+
+ def item_prefix_for(operation)
+ key =
+ # Note: We could have used the right_key here too, they're both the
+ # same keys
+ if operation.respond_to?(:left_key)
+ operation.left_key
+ else
+ operation.key
+ end
+
+ "#{key}: "
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_flatteners/default_object.rb b/lib/super_diff/basic/operation_tree_flatteners/default_object.rb
new file mode 100644
index 00000000..4d994237
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_flatteners/default_object.rb
@@ -0,0 +1,32 @@
+module SuperDiff
+ module Basic
+ module OperationTreeFlatteners
+ class DefaultObject < Collection
+ protected
+
+ def open_token
+ "#<#{operation_tree.underlying_object.class.name}:" +
+ Core::Helpers.object_address_for(operation_tree.underlying_object) +
+ " {"
+ end
+
+ def close_token
+ "}>"
+ end
+
+ def item_prefix_for(operation)
+ key =
+ # Note: We could have used the right_key here too, they're both the
+ # same keys
+ if operation.respond_to?(:left_key)
+ operation.left_key
+ else
+ operation.key
+ end
+
+ "@#{key}="
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_flatteners/hash.rb b/lib/super_diff/basic/operation_tree_flatteners/hash.rb
new file mode 100644
index 00000000..1f35ee23
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_flatteners/hash.rb
@@ -0,0 +1,35 @@
+module SuperDiff
+ module Basic
+ module OperationTreeFlatteners
+ class Hash < Collection
+ protected
+
+ def open_token
+ "{"
+ end
+
+ def close_token
+ "}"
+ end
+
+ def item_prefix_for(operation)
+ key = key_for(operation)
+
+ format_keys_as_kwargs? ? "#{key}: " : "#{key.inspect} => "
+ end
+
+ private
+
+ def format_keys_as_kwargs?
+ operation_tree.all? { |operation| key_for(operation).is_a?(Symbol) }
+ end
+
+ def key_for(operation)
+ # Note: We could have used the right_key here too, they're both the
+ # same keys
+ operation.respond_to?(:left_key) ? operation.left_key : operation.key
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_tree_flatteners/multiline_string.rb b/lib/super_diff/basic/operation_tree_flatteners/multiline_string.rb
new file mode 100644
index 00000000..77048aca
--- /dev/null
+++ b/lib/super_diff/basic/operation_tree_flatteners/multiline_string.rb
@@ -0,0 +1,20 @@
+module SuperDiff
+ module Basic
+ module OperationTreeFlatteners
+ class MultilineString < Core::AbstractOperationTreeFlattener
+ def build_tiered_lines
+ operation_tree.map do |operation|
+ Core::Line.new(
+ type: operation.name,
+ indentation_level: indentation_level,
+ # TODO: Test that quotes and things don't get escaped but escape
+ # characters do
+ value:
+ operation.value.inspect[1..-2].gsub(/\\"/, '"').gsub(/\\'/, "'")
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_trees.rb b/lib/super_diff/basic/operation_trees.rb
new file mode 100644
index 00000000..e505d0b1
--- /dev/null
+++ b/lib/super_diff/basic/operation_trees.rb
@@ -0,0 +1,25 @@
+module SuperDiff
+ module Basic
+ module OperationTrees
+ autoload :Array, "super_diff/basic/operation_trees/array"
+ autoload :CustomObject, "super_diff/basic/operation_trees/custom_object"
+ autoload :DefaultObject, "super_diff/basic/operation_trees/default_object"
+ autoload :Hash, "super_diff/basic/operation_trees/hash"
+ autoload(
+ :MultilineString,
+ "super_diff/basic/operation_trees/multiline_string"
+ )
+
+ class Main
+ def self.call(*args)
+ warn <<~EOT
+ WARNING: SuperDiff::OperationTrees::Main.call(...) is deprecated and will be removed in the next major release.
+ Please use SuperDiff.find_operation_tree_for(...) instead.
+ #{caller_locations.join("\n")}
+ EOT
+ SuperDiff.find_operation_tree_for(*args)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_trees/array.rb b/lib/super_diff/basic/operation_trees/array.rb
new file mode 100644
index 00000000..adc09c42
--- /dev/null
+++ b/lib/super_diff/basic/operation_trees/array.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module OperationTrees
+ class Array < Core::AbstractOperationTree
+ def self.applies_to?(value)
+ value.is_a?(::Array)
+ end
+
+ protected
+
+ def operation_tree_flattener_class
+ OperationTreeFlatteners::Array
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_trees/custom_object.rb b/lib/super_diff/basic/operation_trees/custom_object.rb
new file mode 100644
index 00000000..ce756caa
--- /dev/null
+++ b/lib/super_diff/basic/operation_trees/custom_object.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module OperationTrees
+ class CustomObject < DefaultObject
+ def self.applies_to?(value)
+ value.respond_to?(:attributes_for_super_diff)
+ end
+
+ protected
+
+ def operation_tree_flattener_class
+ OperationTreeFlatteners::CustomObject
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_trees/default_object.rb b/lib/super_diff/basic/operation_trees/default_object.rb
new file mode 100644
index 00000000..57f40a2f
--- /dev/null
+++ b/lib/super_diff/basic/operation_trees/default_object.rb
@@ -0,0 +1,42 @@
+module SuperDiff
+ module Basic
+ module OperationTrees
+ class DefaultObject < Core::AbstractOperationTree
+ def self.applies_to?(*)
+ true
+ end
+
+ attr_reader :underlying_object
+
+ def initialize(operations, underlying_object:)
+ super(operations)
+ @underlying_object = underlying_object
+ end
+
+ def pretty_print(pp)
+ pp.text "#<#{self.class.name} "
+ pp.nest(1) do
+ pp.breakable
+ pp.text ":operations=>"
+ pp.group(1, "[", "]") do
+ pp.breakable
+ pp.seplist(self) { |value| pp.pp value }
+ end
+ pp.comma_breakable
+ pp.text ":underlying_object=>"
+ pp.object_address_group underlying_object do
+ # do nothing
+ end
+ end
+ pp.text ">"
+ end
+
+ protected
+
+ def operation_tree_flattener_class
+ OperationTreeFlatteners::DefaultObject
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_trees/hash.rb b/lib/super_diff/basic/operation_trees/hash.rb
new file mode 100644
index 00000000..d6ced92a
--- /dev/null
+++ b/lib/super_diff/basic/operation_trees/hash.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module OperationTrees
+ class Hash < Core::AbstractOperationTree
+ def self.applies_to?(value)
+ value.is_a?(::Hash)
+ end
+
+ protected
+
+ def operation_tree_flattener_class
+ OperationTreeFlatteners::Hash
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/basic/operation_trees/multiline_string.rb b/lib/super_diff/basic/operation_trees/multiline_string.rb
new file mode 100644
index 00000000..9fd0bd25
--- /dev/null
+++ b/lib/super_diff/basic/operation_trees/multiline_string.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module Basic
+ module OperationTrees
+ class MultilineString < Core::AbstractOperationTree
+ def self.applies_to?(value)
+ value.is_a?(::String) && value.is_a?(::String)
+ end
+
+ protected
+
+ def operation_tree_flattener_class
+ OperationTreeFlatteners::MultilineString
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/colorized_document_extensions.rb b/lib/super_diff/colorized_document_extensions.rb
deleted file mode 100644
index f8cbc1a5..00000000
--- a/lib/super_diff/colorized_document_extensions.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module SuperDiff
- module ColorizedDocumentExtensions
- def self.extended(extendee)
- extendee.singleton_class.class_eval { alias_method :normal, :text }
- end
-
- %i[actual border elision_marker expected header].each do |method_name|
- define_method(method_name) do |*args, **opts, &block|
- colorize(
- *args,
- **opts,
- fg: SuperDiff.configuration.public_send("#{method_name}_color"),
- &block
- )
- end
- end
- end
-end
diff --git a/lib/super_diff/configuration.rb b/lib/super_diff/configuration.rb
deleted file mode 100644
index 5fc1d47e..00000000
--- a/lib/super_diff/configuration.rb
+++ /dev/null
@@ -1,149 +0,0 @@
-module SuperDiff
- class Configuration
- attr_reader(
- :extra_diff_formatter_classes,
- :extra_differ_classes,
- :extra_inspection_tree_builder_classes,
- :extra_operation_tree_builder_classes,
- :extra_operation_tree_classes
- )
- attr_accessor(
- :actual_color,
- :border_color,
- :color_enabled,
- :diff_elision_enabled,
- :diff_elision_maximum,
- :elision_marker_color,
- :expected_color,
- :header_color,
- :key_enabled
- )
-
- def initialize(options = {})
- @actual_color = :yellow
- @border_color = :blue
- @color_enabled = color_enabled_by_default?
- @diff_elision_enabled = false
- @diff_elision_maximum = 0
- @elision_marker_color = :cyan
- @expected_color = :magenta
- @extra_diff_formatter_classes = [].freeze
- @extra_differ_classes = [].freeze
- @extra_inspection_tree_builder_classes = [].freeze
- @extra_operation_tree_builder_classes = [].freeze
- @extra_operation_tree_classes = [].freeze
- @header_color = :white
- @key_enabled = true
-
- merge!(options)
- end
-
- def initialize_dup(original)
- super
- @extra_differ_classes = original.extra_differ_classes.dup.freeze
- @extra_operation_tree_builder_classes =
- original.extra_operation_tree_builder_classes.dup.freeze
- @extra_operation_tree_classes =
- original.extra_operation_tree_classes.dup.freeze
- @extra_inspection_tree_builder_classes =
- original.extra_inspection_tree_builder_classes.dup.freeze
- end
-
- def color_enabled?
- @color_enabled
- end
-
- def diff_elision_enabled?
- @diff_elision_enabled
- end
-
- def key_enabled?
- @key_enabled
- end
-
- def merge!(configuration_or_options)
- options =
- if configuration_or_options.is_a?(self.class)
- configuration_or_options.to_h
- else
- configuration_or_options
- end
-
- options.each { |key, value| instance_variable_set("@#{key}", value) }
-
- updated
- end
-
- def updated
- SuperDiff::Csi.color_enabled = color_enabled?
- end
-
- def add_extra_diff_formatter_classes(*classes)
- @extra_diff_formatter_classes =
- (@extra_diff_formatter_classes + classes).freeze
- end
- alias_method(
- :add_extra_diff_formatter_class,
- :add_extra_diff_formatter_classes
- )
-
- def add_extra_differ_classes(*classes)
- @extra_differ_classes = (@extra_differ_classes + classes).freeze
- end
- alias_method :add_extra_differ_class, :add_extra_differ_classes
-
- def add_extra_inspection_tree_builder_classes(*classes)
- @extra_inspection_tree_builder_classes =
- (@extra_inspection_tree_builder_classes + classes).freeze
- end
- alias_method(
- :add_extra_inspection_tree_builder_class,
- :add_extra_inspection_tree_builder_classes
- )
-
- def add_extra_operation_tree_builder_classes(*classes)
- @extra_operation_tree_builder_classes =
- (@extra_operation_tree_builder_classes + classes).freeze
- end
- alias_method(
- :add_extra_operation_tree_builder_class,
- :add_extra_operation_tree_builder_classes
- )
-
- def add_extra_operation_tree_classes(*classes)
- @extra_operation_tree_classes =
- (@extra_operation_tree_classes + classes).freeze
- end
- alias_method(
- :add_extra_operation_tree_class,
- :add_extra_operation_tree_classes
- )
-
- def to_h
- {
- actual_color: actual_color,
- border_color: border_color,
- color_enabled: color_enabled?,
- diff_elision_enabled: diff_elision_enabled?,
- diff_elision_maximum: diff_elision_maximum,
- elision_marker_color: elision_marker_color,
- expected_color: expected_color,
- extra_diff_formatter_classes: extra_diff_formatter_classes.dup,
- extra_differ_classes: extra_differ_classes.dup,
- extra_inspection_tree_builder_classes:
- extra_inspection_tree_builder_classes.dup,
- extra_operation_tree_builder_classes:
- extra_operation_tree_builder_classes.dup,
- extra_operation_tree_classes: extra_operation_tree_classes.dup,
- header_color: header_color,
- key_enabled: key_enabled?
- }
- end
-
- private
-
- def color_enabled_by_default?
- ENV["CI"] == "true" || $stdout.respond_to?(:tty?) && $stdout.tty?
- end
- end
-end
diff --git a/lib/super_diff/core.rb b/lib/super_diff/core.rb
new file mode 100644
index 00000000..997dd91a
--- /dev/null
+++ b/lib/super_diff/core.rb
@@ -0,0 +1,69 @@
+module SuperDiff
+ module Core
+ autoload :AbstractDiffer, "super_diff/core/abstract_differ"
+ autoload(
+ :AbstractInspectionTreeBuilder,
+ "super_diff/core/abstract_inspection_tree_builder"
+ )
+ autoload :AbstractOperationTree, "super_diff/core/abstract_operation_tree"
+ autoload(
+ :AbstractOperationTreeBuilder,
+ "super_diff/core/abstract_operation_tree_builder"
+ )
+ autoload(
+ :AbstractOperationTreeFlattener,
+ "super_diff/core/abstract_operation_tree_flattener"
+ )
+ autoload :BinaryOperation, "super_diff/core/binary_operation"
+ autoload(
+ :ColorizedDocumentExtensions,
+ "super_diff/core/colorized_document_extensions"
+ )
+ autoload :Configuration, "super_diff/core/configuration"
+ autoload :DifferDispatcher, "super_diff/core/differ_dispatcher"
+ autoload :GemVersion, "super_diff/core/gem_version"
+ autoload :Helpers, "super_diff/core/helpers"
+ autoload :ImplementationChecks, "super_diff/core/implementation_checks"
+ autoload :InspectionTree, "super_diff/core/inspection_tree"
+ autoload(
+ :InspectionTreeBuilderDispatcher,
+ "super_diff/core/inspection_tree_builder_dispatcher"
+ )
+ autoload :InspectionTreeNodes, "super_diff/core/inspection_tree_nodes"
+ autoload :Line, "super_diff/core/line"
+ autoload(
+ :OperationTreeBuilderDispatcher,
+ "super_diff/core/operation_tree_builder_dispatcher"
+ )
+ autoload(
+ :NoDifferAvailableError,
+ "super_diff/core/no_differ_available_error"
+ )
+ autoload(
+ :NoInspectionTreeAvailableError,
+ "super_diff/core/no_inspection_tree_builder_available_error"
+ )
+ autoload(
+ :NoOperationTreeBuilderAvailableError,
+ "super_diff/core/no_operation_tree_builder_available_error"
+ )
+ autoload(
+ :NoOperationTreeAvailableError,
+ "super_diff/core/no_operation_tree_available_error"
+ )
+ autoload :OperationTreeFinder, "super_diff/core/operation_tree_finder"
+ autoload(
+ :PrefixForNextInspectionTreeNode,
+ "super_diff/core/prefix_for_next_inspection_tree_node"
+ )
+ autoload(
+ :PreludeForNextInspectionTreeNode,
+ "super_diff/core/prelude_for_next_inspection_tree_node"
+ )
+ autoload :RecursionGuard, "super_diff/core/recursion_guard"
+ autoload :TieredLines, "super_diff/core/tiered_lines"
+ autoload :TieredLinesElider, "super_diff/core/tiered_lines_elider"
+ autoload :TieredLinesFormatter, "super_diff/core/tiered_lines_formatter"
+ autoload :UnaryOperation, "super_diff/core/unary_operation"
+ end
+end
diff --git a/lib/super_diff/differs/base.rb b/lib/super_diff/core/abstract_differ.rb
similarity index 93%
rename from lib/super_diff/differs/base.rb
rename to lib/super_diff/core/abstract_differ.rb
index 86eca7a1..2bc37eda 100644
--- a/lib/super_diff/differs/base.rb
+++ b/lib/super_diff/core/abstract_differ.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module Differs
- class Base
+ module Core
+ class AbstractDiffer
def self.applies_to?(_expected, _actual)
raise NotImplementedError
end
diff --git a/lib/super_diff/core/abstract_inspection_tree_builder.rb b/lib/super_diff/core/abstract_inspection_tree_builder.rb
new file mode 100644
index 00000000..acbd3f4c
--- /dev/null
+++ b/lib/super_diff/core/abstract_inspection_tree_builder.rb
@@ -0,0 +1,26 @@
+module SuperDiff
+ module Core
+ class AbstractInspectionTreeBuilder
+ extend AttrExtras.mixin
+ extend ImplementationChecks
+ include ImplementationChecks
+ include Helpers
+
+ def self.applies_to?(_value)
+ unimplemented_class_method!
+ end
+
+ method_object :object
+
+ def call
+ unimplemented_instance_method!
+ end
+
+ protected
+
+ def inspection_tree
+ unimplemented_instance_method!
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/operation_trees/base.rb b/lib/super_diff/core/abstract_operation_tree.rb
similarity index 89%
rename from lib/super_diff/operation_trees/base.rb
rename to lib/super_diff/core/abstract_operation_tree.rb
index 2ff34683..f93b1b46 100644
--- a/lib/super_diff/operation_trees/base.rb
+++ b/lib/super_diff/core/abstract_operation_tree.rb
@@ -1,8 +1,8 @@
require "forwardable"
module SuperDiff
- module OperationTrees
- class Base
+ module Core
+ class AbstractOperationTree
def self.applies_to?(*)
unimplemented_class_method!
end
@@ -46,6 +46,10 @@ def perhaps_elide(tiered_lines)
end
end
+ def ==(other)
+ other.is_a?(self.class) && other.to_a == to_a
+ end
+
private
attr_reader :operations
diff --git a/lib/super_diff/operation_tree_builders/base.rb b/lib/super_diff/core/abstract_operation_tree_builder.rb
similarity index 91%
rename from lib/super_diff/operation_tree_builders/base.rb
rename to lib/super_diff/core/abstract_operation_tree_builder.rb
index 24fa35ff..5b8af2ea 100644
--- a/lib/super_diff/operation_tree_builders/base.rb
+++ b/lib/super_diff/core/abstract_operation_tree_builder.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module OperationTreeBuilders
- class Base
+ module Core
+ class AbstractOperationTreeBuilder
def self.applies_to?(_expected, _actual)
raise NotImplementedError
end
@@ -48,7 +48,7 @@ def operation_tree
possible_comparison_of(delete_operation, insert_operation)
)
operation_tree.delete(delete_operation)
- operation_tree << Operations::BinaryOperation.new(
+ operation_tree << BinaryOperation.new(
name: :change,
left_collection: delete_operation.collection,
right_collection: insert_operation.collection,
@@ -89,11 +89,7 @@ def should_compare?(operation, next_operation)
end
def compare(expected, actual)
- OperationTreeBuilders::Main.call(
- expected: expected,
- actual: actual,
- all_or_nothing: false
- )
+ SuperDiff.build_operation_tree_for(expected, actual)
end
end
end
diff --git a/lib/super_diff/operation_tree_flatteners/base.rb b/lib/super_diff/core/abstract_operation_tree_flattener.rb
similarity index 94%
rename from lib/super_diff/operation_tree_flatteners/base.rb
rename to lib/super_diff/core/abstract_operation_tree_flattener.rb
index aaf12d48..f5b4e765 100644
--- a/lib/super_diff/operation_tree_flatteners/base.rb
+++ b/lib/super_diff/core/abstract_operation_tree_flattener.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module OperationTreeFlatteners
- class Base
+ module Core
+ class AbstractOperationTreeFlattener
include ImplementationChecks
extend AttrExtras.mixin
diff --git a/lib/super_diff/operations/binary_operation.rb b/lib/super_diff/core/binary_operation.rb
similarity index 95%
rename from lib/super_diff/operations/binary_operation.rb
rename to lib/super_diff/core/binary_operation.rb
index c31e6635..06bb357d 100644
--- a/lib/super_diff/operations/binary_operation.rb
+++ b/lib/super_diff/core/binary_operation.rb
@@ -1,5 +1,5 @@
module SuperDiff
- module Operations
+ module Core
class BinaryOperation
extend AttrExtras.mixin
diff --git a/lib/super_diff/core/colorized_document_extensions.rb b/lib/super_diff/core/colorized_document_extensions.rb
new file mode 100644
index 00000000..1463157f
--- /dev/null
+++ b/lib/super_diff/core/colorized_document_extensions.rb
@@ -0,0 +1,20 @@
+module SuperDiff
+ module Core
+ module ColorizedDocumentExtensions
+ def self.extended(extendee)
+ extendee.singleton_class.class_eval { alias_method :normal, :text }
+ end
+
+ %i[actual border elision_marker expected header].each do |method_name|
+ define_method(method_name) do |*args, **opts, &block|
+ colorize(
+ *args,
+ **opts,
+ fg: SuperDiff.configuration.public_send("#{method_name}_color"),
+ &block
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/configuration.rb b/lib/super_diff/core/configuration.rb
new file mode 100644
index 00000000..0f5a533d
--- /dev/null
+++ b/lib/super_diff/core/configuration.rb
@@ -0,0 +1,192 @@
+module SuperDiff
+ module Core
+ class Configuration
+ attr_reader(
+ :extra_diff_formatter_classes,
+ :extra_differ_classes,
+ :extra_inspection_tree_builder_classes,
+ :extra_operation_tree_builder_classes,
+ :extra_operation_tree_classes
+ )
+ attr_accessor(
+ :actual_color,
+ :border_color,
+ :color_enabled,
+ :diff_elision_enabled,
+ :diff_elision_maximum,
+ :elision_marker_color,
+ :expected_color,
+ :header_color,
+ :key_enabled
+ )
+
+ def initialize(options = {})
+ @actual_color = :yellow
+ @border_color = :blue
+ @color_enabled = color_enabled_by_default?
+ @diff_elision_enabled = false
+ @diff_elision_maximum = 0
+ @elision_marker_color = :cyan
+ @expected_color = :magenta
+ @extra_diff_formatter_classes = [].freeze
+ @extra_differ_classes = [].freeze
+ @extra_inspection_tree_builder_classes = [].freeze
+ @extra_operation_tree_builder_classes = [].freeze
+ @extra_operation_tree_classes = [].freeze
+ @header_color = :white
+ @key_enabled = true
+
+ merge!(options)
+ end
+
+ def initialize_dup(original)
+ super
+ @extra_differ_classes = original.extra_differ_classes.dup.freeze
+ @extra_operation_tree_builder_classes =
+ original.extra_operation_tree_builder_classes.dup.freeze
+ @extra_operation_tree_classes =
+ original.extra_operation_tree_classes.dup.freeze
+ @extra_inspection_tree_builder_classes =
+ original.extra_inspection_tree_builder_classes.dup.freeze
+ end
+
+ def color_enabled?
+ @color_enabled
+ end
+
+ def diff_elision_enabled?
+ @diff_elision_enabled
+ end
+
+ def key_enabled?
+ @key_enabled
+ end
+
+ def merge!(configuration_or_options)
+ options =
+ if configuration_or_options.is_a?(self.class)
+ configuration_or_options.to_h
+ else
+ configuration_or_options
+ end
+
+ options.each { |key, value| instance_variable_set("@#{key}", value) }
+
+ updated
+ end
+
+ def updated
+ SuperDiff::Csi.color_enabled = color_enabled?
+ end
+
+ def add_extra_diff_formatter_classes(*classes)
+ @extra_diff_formatter_classes =
+ (@extra_diff_formatter_classes + classes).freeze
+ end
+ alias_method(
+ :add_extra_diff_formatter_class,
+ :add_extra_diff_formatter_classes
+ )
+
+ def prepend_extra_diff_formatter_classes(*classes)
+ @extra_diff_formatter_classes =
+ (classes + @extra_diff_formatter_classes).freeze
+ end
+ alias_method(
+ :prepend_extra_diff_formatter_class,
+ :prepend_extra_diff_formatter_classes
+ )
+
+ def add_extra_differ_classes(*classes)
+ @extra_differ_classes = (@extra_differ_classes + classes).freeze
+ end
+ alias_method :add_extra_differ_class, :add_extra_differ_classes
+
+ def prepend_extra_differ_classes(*classes)
+ @extra_differ_classes = (classes + @extra_differ_classes).freeze
+ end
+ alias_method :prepend_extra_differ_class, :prepend_extra_differ_classes
+
+ def add_extra_inspection_tree_builder_classes(*classes)
+ @extra_inspection_tree_builder_classes =
+ (@extra_inspection_tree_builder_classes + classes).freeze
+ end
+ alias_method(
+ :add_extra_inspection_tree_builder_class,
+ :add_extra_inspection_tree_builder_classes
+ )
+
+ def prepend_extra_inspection_tree_builder_classes(*classes)
+ @extra_inspection_tree_builder_classes =
+ (classes + @extra_inspection_tree_builder_classes).freeze
+ end
+ alias_method(
+ :prepend_extra_inspection_tree_builder_class,
+ :prepend_extra_inspection_tree_builder_classes
+ )
+
+ def add_extra_operation_tree_builder_classes(*classes)
+ @extra_operation_tree_builder_classes =
+ (@extra_operation_tree_builder_classes + classes).freeze
+ end
+ alias_method(
+ :add_extra_operation_tree_builder_class,
+ :add_extra_operation_tree_builder_classes
+ )
+
+ def prepend_extra_operation_tree_builder_classes(*classes)
+ @extra_operation_tree_builder_classes =
+ (classes + @extra_operation_tree_builder_classes).freeze
+ end
+ alias_method(
+ :prepend_extra_operation_tree_builder_class,
+ :prepend_extra_operation_tree_builder_classes
+ )
+
+ def add_extra_operation_tree_classes(*classes)
+ @extra_operation_tree_classes =
+ (@extra_operation_tree_classes + classes).freeze
+ end
+ alias_method(
+ :add_extra_operation_tree_class,
+ :add_extra_operation_tree_classes
+ )
+
+ def prepend_extra_operation_tree_classes(*classes)
+ @extra_operation_tree_classes =
+ (classes + @extra_operation_tree_classes).freeze
+ end
+ alias_method(
+ :prepend_extra_operation_tree_class,
+ :prepend_extra_operation_tree_classes
+ )
+
+ def to_h
+ {
+ actual_color: actual_color,
+ border_color: border_color,
+ color_enabled: color_enabled?,
+ diff_elision_enabled: diff_elision_enabled?,
+ diff_elision_maximum: diff_elision_maximum,
+ elision_marker_color: elision_marker_color,
+ expected_color: expected_color,
+ extra_diff_formatter_classes: extra_diff_formatter_classes.dup,
+ extra_differ_classes: extra_differ_classes.dup,
+ extra_inspection_tree_builder_classes:
+ extra_inspection_tree_builder_classes.dup,
+ extra_operation_tree_builder_classes:
+ extra_operation_tree_builder_classes.dup,
+ extra_operation_tree_classes: extra_operation_tree_classes.dup,
+ header_color: header_color,
+ key_enabled: key_enabled?
+ }
+ end
+
+ private
+
+ def color_enabled_by_default?
+ ENV["CI"] == "true" || $stdout.respond_to?(:tty?) && $stdout.tty?
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/differ_dispatcher.rb b/lib/super_diff/core/differ_dispatcher.rb
new file mode 100644
index 00000000..6f0a368c
--- /dev/null
+++ b/lib/super_diff/core/differ_dispatcher.rb
@@ -0,0 +1,31 @@
+module SuperDiff
+ module Core
+ class DifferDispatcher
+ extend AttrExtras.mixin
+
+ method_object(
+ :expected,
+ :actual,
+ [:available_classes, indent_level: 0, raise_if_nothing_applies: true]
+ )
+
+ def call
+ if resolved_class
+ resolved_class.call(expected, actual, indent_level: indent_level)
+ elsif raise_if_nothing_applies?
+ raise NoDifferAvailableError.create(expected, actual)
+ else
+ ""
+ end
+ end
+
+ private
+
+ attr_query :raise_if_nothing_applies?
+
+ def resolved_class
+ available_classes.find { |klass| klass.applies_to?(expected, actual) }
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/gem_version.rb b/lib/super_diff/core/gem_version.rb
new file mode 100644
index 00000000..e75a54c4
--- /dev/null
+++ b/lib/super_diff/core/gem_version.rb
@@ -0,0 +1,47 @@
+module SuperDiff
+ module Core
+ class GemVersion
+ def initialize(version)
+ @version = Gem::Version.new(version.to_s)
+ end
+
+ def <(other)
+ compare?(:<, other)
+ end
+
+ def <=(other)
+ compare?(:<=, other)
+ end
+
+ def ==(other)
+ compare?(:==, other)
+ end
+
+ def >=(other)
+ compare?(:>=, other)
+ end
+
+ def >(other)
+ compare?(:>, other)
+ end
+
+ def =~(other)
+ Gem::Requirement.new(other).satisfied_by?(version)
+ end
+
+ def to_s
+ version.to_s
+ end
+
+ private
+
+ attr_reader :version
+
+ def compare?(operator, other_version)
+ Gem::Requirement.new("#{operator} #{other_version}").satisfied_by?(
+ version
+ )
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/helpers.rb b/lib/super_diff/core/helpers.rb
new file mode 100644
index 00000000..35dcc979
--- /dev/null
+++ b/lib/super_diff/core/helpers.rb
@@ -0,0 +1,88 @@
+module SuperDiff
+ module Core
+ module Helpers
+ extend self
+
+ # TODO: Simplify this
+ def style(*args, color_enabled: true, **opts, &block)
+ klass =
+ if color_enabled && Csi.color_enabled?
+ Csi::ColorizedDocument
+ else
+ Csi::UncolorizedDocument
+ end
+
+ document = klass.new.extend(ColorizedDocumentExtensions)
+
+ if block
+ document.__send__(:evaluate_block, &block)
+ else
+ document.colorize(*args, **opts)
+ end
+
+ document
+ end
+
+ def plural_type_for(value)
+ case value
+ when Numeric
+ "numbers"
+ when String
+ "strings"
+ when Symbol
+ "symbols"
+ else
+ "objects"
+ end
+ end
+
+ def jruby?
+ defined?(JRUBY_VERSION)
+ end
+
+ def ruby_version_matches?(version_string)
+ Gem::Requirement.new(version_string).satisfied_by?(
+ Gem::Version.new(RUBY_VERSION)
+ )
+ end
+
+ if jruby?
+ def object_address_for(object)
+ # Source:
+ "0x%x" % object.hash
+ end
+ elsif ruby_version_matches?(">= 2.7.0")
+ require "json"
+ require "objspace"
+
+ def object_address_for(object)
+ # Sources: and
+ json = JSON.parse(ObjectSpace.dump(object))
+ json.is_a?(Hash) ? "0x%016x" % Integer(json["address"], 16) : ""
+ end
+ else
+ def object_address_for(object)
+ "0x%016x" % (object.object_id * 2)
+ end
+ end
+
+ def with_slice_of_array_replaced(array, range, replacement)
+ beginning =
+ if range.begin > 0
+ array[Range.new(0, range.begin - 1)]
+ else
+ []
+ end
+
+ ending =
+ if range.end <= array.length - 1
+ array[Range.new(range.end + 1, array.length - 1)]
+ else
+ []
+ end
+
+ beginning + [replacement] + ending
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/implementation_checks.rb b/lib/super_diff/core/implementation_checks.rb
new file mode 100644
index 00000000..5ebb371c
--- /dev/null
+++ b/lib/super_diff/core/implementation_checks.rb
@@ -0,0 +1,21 @@
+module SuperDiff
+ module Core
+ module ImplementationChecks
+ protected def unimplemented_instance_method!
+ raise(
+ NotImplementedError,
+ "#{self.class} must implement ##{caller_locations(1, 1).first.label}",
+ caller(1)
+ )
+ end
+
+ protected def unimplemented_class_method!
+ raise(
+ NotImplementedError,
+ "#{self} must implement .#{caller_locations(1, 1).first.label}",
+ caller(1)
+ )
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/object_inspection/inspection_tree.rb b/lib/super_diff/core/inspection_tree.rb
similarity index 94%
rename from lib/super_diff/object_inspection/inspection_tree.rb
rename to lib/super_diff/core/inspection_tree.rb
index 2df63022..f49c75fc 100644
--- a/lib/super_diff/object_inspection/inspection_tree.rb
+++ b/lib/super_diff/core/inspection_tree.rb
@@ -1,5 +1,5 @@
module SuperDiff
- module ObjectInspection
+ module Core
class InspectionTree
include Enumerable
@@ -10,7 +10,7 @@ def initialize(disallowed_node_names: [], &block)
evaluate_block(&block) if block
end
- Nodes.registry.each do |node_class|
+ InspectionTreeNodes.registry.each do |node_class|
define_method(node_class.method_name) do |*args, **options, &block|
add_node(node_class, *args, **options, &block)
end
@@ -59,7 +59,7 @@ def insert_array_inspection_of(array)
insert_separated_list(array) do |t, value|
# Have to do these shenanigans so that if value is a hash, Ruby
# doesn't try to interpret it as keyword args
- if SuperDiff::Helpers.ruby_version_matches?(">= 2.7.1")
+ if Helpers.ruby_version_matches?(">= 2.7.1")
t.add_inspection_of(value, **{})
else
t.add_inspection_of(*[value, {}])
@@ -86,7 +86,7 @@ def insert_hash_inspection_of(hash)
# Have to do these shenanigans so that if hash[key] is a hash, Ruby
# doesn't try to interpret it as keyword args
- if SuperDiff::Helpers.ruby_version_matches?(">= 2.7.1")
+ if Helpers.ruby_version_matches?(">= 2.7.1")
t1.add_inspection_of(hash[key], **{})
else
t1.add_inspection_of(*[hash[key], {}])
@@ -148,11 +148,12 @@ class UpdateTieredLines
def call
if rendering.is_a?(Array)
concat_with_lines
- elsif rendering.is_a?(PrefixForNextNode)
+ elsif rendering.is_a?(PrefixForNextInspectionTreeNode)
add_to_prefix
elsif tiered_lines.any?
add_to_last_line
- elsif index < nodes.size - 1 || rendering.is_a?(PreludeForNextNode)
+ elsif index < nodes.size - 1 ||
+ rendering.is_a?(PreludeForNextInspectionTreeNode)
add_to_prelude
else
add_to_lines
diff --git a/lib/super_diff/core/inspection_tree_builder_dispatcher.rb b/lib/super_diff/core/inspection_tree_builder_dispatcher.rb
new file mode 100644
index 00000000..c357a400
--- /dev/null
+++ b/lib/super_diff/core/inspection_tree_builder_dispatcher.rb
@@ -0,0 +1,23 @@
+module SuperDiff
+ module Core
+ class InspectionTreeBuilderDispatcher
+ extend AttrExtras.mixin
+
+ method_object :object, [:available_classes]
+
+ def call
+ if resolved_class
+ resolved_class.call(object)
+ else
+ raise NoInspectionTreeBuilderAvailableError.create(object)
+ end
+ end
+
+ private
+
+ def resolved_class
+ available_classes.find { |klass| klass.applies_to?(object) }
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/inspection_tree_nodes.rb b/lib/super_diff/core/inspection_tree_nodes.rb
new file mode 100644
index 00000000..8c801579
--- /dev/null
+++ b/lib/super_diff/core/inspection_tree_nodes.rb
@@ -0,0 +1,55 @@
+module SuperDiff
+ module Core
+ module InspectionTreeNodes
+ autoload(
+ :AsLinesWhenRenderingToLines,
+ "super_diff/core/inspection_tree_nodes/as_lines_when_rendering_to_lines"
+ )
+ autoload(
+ :AsPrefixWhenRenderingToLines,
+ "super_diff/core/inspection_tree_nodes/as_prefix_when_rendering_to_lines"
+ )
+ autoload(
+ :AsPreludeWhenRenderingToLines,
+ "super_diff/core/inspection_tree_nodes/as_prelude_when_rendering_to_lines"
+ )
+ autoload(
+ :AsSingleLine,
+ "super_diff/core/inspection_tree_nodes/as_single_line"
+ )
+ autoload :Base, "super_diff/core/inspection_tree_nodes/base"
+ autoload :Inspection, "super_diff/core/inspection_tree_nodes/inspection"
+ autoload :Nesting, "super_diff/core/inspection_tree_nodes/nesting"
+ autoload :OnlyWhen, "super_diff/core/inspection_tree_nodes/only_when"
+ autoload :Text, "super_diff/core/inspection_tree_nodes/text"
+ autoload :WhenEmpty, "super_diff/core/inspection_tree_nodes/when_empty"
+ autoload(
+ :WhenNonEmpty,
+ "super_diff/core/inspection_tree_nodes/when_non_empty"
+ )
+ autoload(
+ :WhenRenderingToLines,
+ "super_diff/core/inspection_tree_nodes/when_rendering_to_lines"
+ )
+ autoload(
+ :WhenRenderingToString,
+ "super_diff/core/inspection_tree_nodes/when_rendering_to_string"
+ )
+
+ def self.registry
+ @_registry ||= [
+ AsLinesWhenRenderingToLines,
+ AsPrefixWhenRenderingToLines,
+ AsPreludeWhenRenderingToLines,
+ AsSingleLine,
+ Inspection,
+ Nesting,
+ OnlyWhen,
+ Text,
+ WhenRenderingToLines,
+ WhenRenderingToString
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/object_inspection/nodes/as_lines_when_rendering_to_lines.rb b/lib/super_diff/core/inspection_tree_nodes/as_lines_when_rendering_to_lines.rb
similarity index 90%
rename from lib/super_diff/object_inspection/nodes/as_lines_when_rendering_to_lines.rb
rename to lib/super_diff/core/inspection_tree_nodes/as_lines_when_rendering_to_lines.rb
index f8a13158..26476929 100644
--- a/lib/super_diff/object_inspection/nodes/as_lines_when_rendering_to_lines.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/as_lines_when_rendering_to_lines.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class AsLinesWhenRenderingToLines < Base
def self.node_name
:as_lines_when_rendering_to_lines
@@ -27,7 +27,13 @@ def initialize(
def render_to_string(object)
# TODO: This happens a lot, can we simplify this?
string =
- (block ? render_to_string_in_subtree(object) : immediate_value.to_s)
+ (
+ if block
+ render_to_string_in_subtree(object)
+ else
+ immediate_value.to_s
+ end
+ )
add_comma? ? string + "," : string
end
diff --git a/lib/super_diff/object_inspection/nodes/as_prefix_when_rendering_to_lines.rb b/lib/super_diff/core/inspection_tree_nodes/as_prefix_when_rendering_to_lines.rb
similarity index 78%
rename from lib/super_diff/object_inspection/nodes/as_prefix_when_rendering_to_lines.rb
rename to lib/super_diff/core/inspection_tree_nodes/as_prefix_when_rendering_to_lines.rb
index 28262ecd..592ca9f8 100644
--- a/lib/super_diff/object_inspection/nodes/as_prefix_when_rendering_to_lines.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/as_prefix_when_rendering_to_lines.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class AsPrefixWhenRenderingToLines < Base
def self.name
:as_prefix_when_rendering_to_lines
@@ -15,7 +15,7 @@ def render_to_string(object)
end
def render_to_lines(object, **)
- ObjectInspection::PrefixForNextNode.new(render_to_string(object))
+ PrefixForNextInspectionTreeNode.new(render_to_string(object))
end
end
end
diff --git a/lib/super_diff/object_inspection/nodes/as_prelude_when_rendering_to_lines.rb b/lib/super_diff/core/inspection_tree_nodes/as_prelude_when_rendering_to_lines.rb
similarity index 78%
rename from lib/super_diff/object_inspection/nodes/as_prelude_when_rendering_to_lines.rb
rename to lib/super_diff/core/inspection_tree_nodes/as_prelude_when_rendering_to_lines.rb
index df5c8dde..8ca74a36 100644
--- a/lib/super_diff/object_inspection/nodes/as_prelude_when_rendering_to_lines.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/as_prelude_when_rendering_to_lines.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class AsPreludeWhenRenderingToLines < Base
def self.name
:as_prelude_when_rendering_to_lines
@@ -15,7 +15,7 @@ def render_to_string(object)
end
def render_to_lines(object, **)
- ObjectInspection::PreludeForNextNode.new(render_to_string(object))
+ PreludeForNextInspectionTreeNode.new(render_to_string(object))
end
end
end
diff --git a/lib/super_diff/object_inspection/nodes/as_single_line.rb b/lib/super_diff/core/inspection_tree_nodes/as_single_line.rb
similarity index 88%
rename from lib/super_diff/object_inspection/nodes/as_single_line.rb
rename to lib/super_diff/core/inspection_tree_nodes/as_single_line.rb
index 447818fa..6b4b9fcc 100644
--- a/lib/super_diff/object_inspection/nodes/as_single_line.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/as_single_line.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class AsSingleLine < Base
def self.node_name
:as_single_line
@@ -16,7 +16,7 @@ def render_to_string(object)
def render_to_lines(object, type:, indentation_level:)
[
- SuperDiff::Line.new(
+ Line.new(
type: type,
indentation_level: indentation_level,
value: render_to_string(object)
diff --git a/lib/super_diff/object_inspection/nodes/base.rb b/lib/super_diff/core/inspection_tree_nodes/base.rb
similarity index 98%
rename from lib/super_diff/object_inspection/nodes/base.rb
rename to lib/super_diff/core/inspection_tree_nodes/base.rb
index 51618e50..71adbeeb 100644
--- a/lib/super_diff/object_inspection/nodes/base.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/base.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class Base
def self.node_name
unimplemented_class_method!
diff --git a/lib/super_diff/object_inspection/nodes/inspection.rb b/lib/super_diff/core/inspection_tree_nodes/inspection.rb
similarity index 71%
rename from lib/super_diff/object_inspection/nodes/inspection.rb
rename to lib/super_diff/core/inspection_tree_nodes/inspection.rb
index e457737c..a8d5b5c2 100644
--- a/lib/super_diff/object_inspection/nodes/inspection.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/inspection.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class Inspection < Base
def self.node_name
:inspection
@@ -13,11 +13,9 @@ def self.method_name
def render_to_string(object)
value = (block ? evaluate_block(object) : immediate_value)
- SuperDiff::RecursionGuard.guarding_recursion_of(
- value
- ) do |already_seen|
+ RecursionGuard.guarding_recursion_of(value) do |already_seen|
if already_seen
- SuperDiff::RecursionGuard::PLACEHOLDER
+ RecursionGuard::PLACEHOLDER
else
SuperDiff.inspect_object(value, as_lines: false)
end
@@ -27,15 +25,13 @@ def render_to_string(object)
def render_to_lines(object, type:, indentation_level:)
value = (block ? evaluate_block(object) : immediate_value)
- SuperDiff::RecursionGuard.guarding_recursion_of(
- value
- ) do |already_seen|
+ RecursionGuard.guarding_recursion_of(value) do |already_seen|
if already_seen
[
- SuperDiff::Line.new(
+ SuperDiff::Core::Line.new(
type: type,
indentation_level: indentation_level,
- value: SuperDiff::RecursionGuard::PLACEHOLDER
+ value: RecursionGuard::PLACEHOLDER
)
]
else
diff --git a/lib/super_diff/object_inspection/nodes/nesting.rb b/lib/super_diff/core/inspection_tree_nodes/nesting.rb
similarity index 91%
rename from lib/super_diff/object_inspection/nodes/nesting.rb
rename to lib/super_diff/core/inspection_tree_nodes/nesting.rb
index c8908553..ef7875c2 100644
--- a/lib/super_diff/object_inspection/nodes/nesting.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/nesting.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class Nesting < Base
def self.node_name
:nesting
diff --git a/lib/super_diff/object_inspection/nodes/only_when.rb b/lib/super_diff/core/inspection_tree_nodes/only_when.rb
similarity index 95%
rename from lib/super_diff/object_inspection/nodes/only_when.rb
rename to lib/super_diff/core/inspection_tree_nodes/only_when.rb
index 453e6e2b..953678a5 100644
--- a/lib/super_diff/object_inspection/nodes/only_when.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/only_when.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class OnlyWhen < Base
def self.node_name
:only_when
diff --git a/lib/super_diff/object_inspection/nodes/text.rb b/lib/super_diff/core/inspection_tree_nodes/text.rb
similarity index 92%
rename from lib/super_diff/object_inspection/nodes/text.rb
rename to lib/super_diff/core/inspection_tree_nodes/text.rb
index e7317f07..30260de4 100644
--- a/lib/super_diff/object_inspection/nodes/text.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/text.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class Text < Base
def self.node_name
:text
diff --git a/lib/super_diff/object_inspection/nodes/when_empty.rb b/lib/super_diff/core/inspection_tree_nodes/when_empty.rb
similarity index 94%
rename from lib/super_diff/object_inspection/nodes/when_empty.rb
rename to lib/super_diff/core/inspection_tree_nodes/when_empty.rb
index cebe591e..a620f36b 100644
--- a/lib/super_diff/object_inspection/nodes/when_empty.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/when_empty.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class WhenEmpty < Base
def self.node_name
:when_empty
diff --git a/lib/super_diff/object_inspection/nodes/when_non_empty.rb b/lib/super_diff/core/inspection_tree_nodes/when_non_empty.rb
similarity index 94%
rename from lib/super_diff/object_inspection/nodes/when_non_empty.rb
rename to lib/super_diff/core/inspection_tree_nodes/when_non_empty.rb
index b3c875d1..c6685909 100644
--- a/lib/super_diff/object_inspection/nodes/when_non_empty.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/when_non_empty.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class WhenNonEmpty < Base
def self.node_name
:when_non_empty
diff --git a/lib/super_diff/object_inspection/nodes/when_rendering_to_lines.rb b/lib/super_diff/core/inspection_tree_nodes/when_rendering_to_lines.rb
similarity index 91%
rename from lib/super_diff/object_inspection/nodes/when_rendering_to_lines.rb
rename to lib/super_diff/core/inspection_tree_nodes/when_rendering_to_lines.rb
index 1132c6bf..8cb8747f 100644
--- a/lib/super_diff/object_inspection/nodes/when_rendering_to_lines.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/when_rendering_to_lines.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class WhenRenderingToLines < Base
def self.node_name
:when_rendering_to_lines
diff --git a/lib/super_diff/object_inspection/nodes/when_rendering_to_string.rb b/lib/super_diff/core/inspection_tree_nodes/when_rendering_to_string.rb
similarity index 90%
rename from lib/super_diff/object_inspection/nodes/when_rendering_to_string.rb
rename to lib/super_diff/core/inspection_tree_nodes/when_rendering_to_string.rb
index 8ee14d1d..b6bfc130 100644
--- a/lib/super_diff/object_inspection/nodes/when_rendering_to_string.rb
+++ b/lib/super_diff/core/inspection_tree_nodes/when_rendering_to_string.rb
@@ -1,6 +1,6 @@
module SuperDiff
- module ObjectInspection
- module Nodes
+ module Core
+ module InspectionTreeNodes
class WhenRenderingToString < Base
def self.node_name
:when_rendering_to_string
diff --git a/lib/super_diff/core/line.rb b/lib/super_diff/core/line.rb
new file mode 100644
index 00000000..b88903cb
--- /dev/null
+++ b/lib/super_diff/core/line.rb
@@ -0,0 +1,85 @@
+module SuperDiff
+ module Core
+ class Line
+ extend AttrExtras.mixin
+
+ ICONS = { delete: "-", insert: "+", noop: " " }.freeze
+ COLORS = { insert: :actual, delete: :expected, noop: :plain }.freeze
+
+ rattr_initialize(
+ [
+ :type!,
+ :indentation_level!,
+ :value!,
+ prefix: "",
+ add_comma: false,
+ children: [],
+ elided: false,
+ collection_bookend: nil,
+ complete_bookend: nil
+ ]
+ )
+ attr_query :add_comma?
+ attr_query :elided?
+
+ def clone_with(overrides = {})
+ self.class.new(
+ type: type,
+ indentation_level: indentation_level,
+ prefix: prefix,
+ value: value,
+ add_comma: add_comma?,
+ children: children,
+ elided: elided?,
+ collection_bookend: collection_bookend,
+ complete_bookend: complete_bookend,
+ **overrides
+ )
+ end
+
+ def icon
+ ICONS.fetch(type)
+ end
+
+ def color
+ COLORS.fetch(type)
+ end
+
+ def with_comma
+ clone_with(add_comma: true)
+ end
+
+ def as_elided
+ clone_with(elided: true)
+ end
+
+ def with_value_prepended(prelude)
+ clone_with(value: prelude + value)
+ end
+
+ def with_value_appended(suffix)
+ clone_with(value: value + suffix)
+ end
+
+ def prefixed_with(prefix)
+ clone_with(prefix: prefix + self.prefix)
+ end
+
+ def with_complete_bookend(complete_bookend)
+ clone_with(complete_bookend: complete_bookend)
+ end
+
+ def opens_collection?
+ collection_bookend == :open
+ end
+
+ def closes_collection?
+ collection_bookend == :close
+ end
+
+ def complete_bookend?
+ complete_bookend != nil
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/errors/no_differ_available_error.rb b/lib/super_diff/core/no_differ_available_error.rb
similarity index 97%
rename from lib/super_diff/errors/no_differ_available_error.rb
rename to lib/super_diff/core/no_differ_available_error.rb
index 902271da..029bb03a 100644
--- a/lib/super_diff/errors/no_differ_available_error.rb
+++ b/lib/super_diff/core/no_differ_available_error.rb
@@ -1,5 +1,5 @@
module SuperDiff
- module Errors
+ module Core
class NoDifferAvailableError < StandardError
def self.create(expected, actual)
allocate.tap do |error|
diff --git a/lib/super_diff/core/no_inspection_tree_builder_available_error.rb b/lib/super_diff/core/no_inspection_tree_builder_available_error.rb
new file mode 100644
index 00000000..8cdaec33
--- /dev/null
+++ b/lib/super_diff/core/no_inspection_tree_builder_available_error.rb
@@ -0,0 +1,21 @@
+module SuperDiff
+ module Core
+ class NoInspectionTreeBuilderAvailableError < StandardError
+ def self.create(object)
+ allocate.tap do |error|
+ error.object = object
+ error.__send__(:initialize)
+ end
+ end
+
+ attr_accessor :object
+
+ def initialize
+ super(<<-MESSAGE)
+There is no inspection tree builder available to handle a "value" of type
+#{object.class}.
+ MESSAGE
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/no_operation_tree_available_error.rb b/lib/super_diff/core/no_operation_tree_available_error.rb
new file mode 100644
index 00000000..6adf7a9e
--- /dev/null
+++ b/lib/super_diff/core/no_operation_tree_available_error.rb
@@ -0,0 +1,20 @@
+module SuperDiff
+ module Core
+ class NoOperationTreeAvailableError < StandardError
+ def self.create(value)
+ allocate.tap do |error|
+ error.value = value
+ error.__send__(:initialize)
+ end
+ end
+
+ attr_accessor :value
+
+ def initialize
+ super(<<-MESSAGE)
+There is no operation tree available to handle a "value" of type #{value.class}.
+ MESSAGE
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/no_operation_tree_builder_available_error.rb b/lib/super_diff/core/no_operation_tree_builder_available_error.rb
new file mode 100644
index 00000000..aae6f042
--- /dev/null
+++ b/lib/super_diff/core/no_operation_tree_builder_available_error.rb
@@ -0,0 +1,24 @@
+module SuperDiff
+ module Core
+ class NoOperationTreeBuilderAvailableError < StandardError
+ def self.create(expected, actual)
+ allocate.tap do |error|
+ error.expected = expected
+ error.actual = actual
+ error.__send__(:initialize)
+ end
+ end
+
+ attr_accessor :expected, :actual
+
+ def initialize
+ super(<<-MESSAGE)
+There is no operation tree builder available to handle an "expected" value of type
+#{expected.class}
+and an "actual" value of type
+#{actual.class}.
+ MESSAGE
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/operation_tree_builders/main.rb b/lib/super_diff/core/operation_tree_builder_dispatcher.rb
similarity index 53%
rename from lib/super_diff/operation_tree_builders/main.rb
rename to lib/super_diff/core/operation_tree_builder_dispatcher.rb
index 6a1a9b54..344b3b17 100644
--- a/lib/super_diff/operation_tree_builders/main.rb
+++ b/lib/super_diff/core/operation_tree_builder_dispatcher.rb
@@ -1,14 +1,18 @@
module SuperDiff
- module OperationTreeBuilders
- class Main
+ module Core
+ class OperationTreeBuilderDispatcher
extend AttrExtras.mixin
- method_object %i[expected! actual! all_or_nothing!]
+ method_object(
+ :expected,
+ :actual,
+ [:available_classes, raise_if_nothing_applies: true]
+ )
def call
if resolved_class
resolved_class.call(expected: expected, actual: actual)
- elsif all_or_nothing?
+ elsif raise_if_nothing_applies?
raise NoOperationTreeBuilderAvailableError.create(expected, actual)
else
nil
@@ -17,19 +21,11 @@ def call
private
- attr_query :all_or_nothing?
+ attr_query :raise_if_nothing_applies?
def resolved_class
available_classes.find { |klass| klass.applies_to?(expected, actual) }
end
-
- def available_classes
- classes =
- SuperDiff.configuration.extra_operation_tree_builder_classes +
- DEFAULTS
-
- all_or_nothing? ? classes + [DefaultObject] : classes
- end
end
end
end
diff --git a/lib/super_diff/core/operation_tree_finder.rb b/lib/super_diff/core/operation_tree_finder.rb
new file mode 100644
index 00000000..6d981a06
--- /dev/null
+++ b/lib/super_diff/core/operation_tree_finder.rb
@@ -0,0 +1,27 @@
+module SuperDiff
+ module Core
+ class OperationTreeFinder
+ extend AttrExtras.mixin
+
+ method_object :value, [:available_classes]
+
+ def call
+ if resolved_class
+ begin
+ resolved_class.new([], underlying_object: value)
+ rescue ArgumentError
+ resolved_class.new([])
+ end
+ else
+ raise NoOperationTreeAvailableError.create(value)
+ end
+ end
+
+ private
+
+ def resolved_class
+ available_classes.find { |klass| klass.applies_to?(value) }
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/prefix_for_next_inspection_tree_node.rb b/lib/super_diff/core/prefix_for_next_inspection_tree_node.rb
new file mode 100644
index 00000000..5f47499d
--- /dev/null
+++ b/lib/super_diff/core/prefix_for_next_inspection_tree_node.rb
@@ -0,0 +1,6 @@
+module SuperDiff
+ module Core
+ class PrefixForNextInspectionTreeNode < String
+ end
+ end
+end
diff --git a/lib/super_diff/core/prelude_for_next_inspection_tree_node.rb b/lib/super_diff/core/prelude_for_next_inspection_tree_node.rb
new file mode 100644
index 00000000..7e8215ea
--- /dev/null
+++ b/lib/super_diff/core/prelude_for_next_inspection_tree_node.rb
@@ -0,0 +1,6 @@
+module SuperDiff
+ module Core
+ class PreludeForNextInspectionTreeNode < String
+ end
+ end
+end
diff --git a/lib/super_diff/core/recursion_guard.rb b/lib/super_diff/core/recursion_guard.rb
new file mode 100644
index 00000000..68402985
--- /dev/null
+++ b/lib/super_diff/core/recursion_guard.rb
@@ -0,0 +1,52 @@
+require "set"
+
+module SuperDiff
+ module Core
+ module RecursionGuard
+ RECURSION_GUARD_KEY = "super_diff_recursion_guard_key".freeze
+ PLACEHOLDER = "âââ".freeze
+
+ def self.guarding_recursion_of(*objects, &block)
+ already_seen_objects, first_seen_objects =
+ objects.partition do |object|
+ !SuperDiff.primitive?(object) && already_seen?(object)
+ end
+
+ first_seen_objects.each do |object|
+ already_seen_object_ids.add(object.object_id)
+ end
+
+ result =
+ if block.arity > 0
+ block.call(already_seen_objects.any?)
+ else
+ block.call
+ end
+
+ first_seen_objects.each do |object|
+ already_seen_object_ids.delete(object.object_id)
+ end
+
+ result
+ end
+
+ def self.substituting_recursion_of(*objects)
+ guarding_recursion_of(*objects) do |already_seen|
+ if already_seen
+ PLACEHOLDER
+ else
+ yield
+ end
+ end
+ end
+
+ def self.already_seen?(object)
+ already_seen_object_ids.include?(object.object_id)
+ end
+
+ def self.already_seen_object_ids
+ Thread.current[RECURSION_GUARD_KEY] ||= Set.new
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/tiered_lines.rb b/lib/super_diff/core/tiered_lines.rb
new file mode 100644
index 00000000..e921f681
--- /dev/null
+++ b/lib/super_diff/core/tiered_lines.rb
@@ -0,0 +1,6 @@
+module SuperDiff
+ module Core
+ class TieredLines < Array
+ end
+ end
+end
diff --git a/lib/super_diff/core/tiered_lines_elider.rb b/lib/super_diff/core/tiered_lines_elider.rb
new file mode 100644
index 00000000..8fcc280d
--- /dev/null
+++ b/lib/super_diff/core/tiered_lines_elider.rb
@@ -0,0 +1,472 @@
+module SuperDiff
+ module Core
+ class TieredLinesElider
+ SIZE_OF_ELISION = 1
+
+ extend AttrExtras.mixin
+ include Helpers
+
+ method_object :lines
+
+ def call
+ all_lines_are_changed_or_unchanged? ? lines : elided_lines
+ end
+
+ private
+
+ def all_lines_are_changed_or_unchanged?
+ panes.size == 1 && panes.first.range == Range.new(0, lines.length - 1)
+ end
+
+ def elided_lines
+ boxes_to_elide
+ .reverse
+ .reduce(lines) do |lines_with_elisions, box|
+ with_box_elided(box, lines_with_elisions)
+ end
+ end
+
+ def boxes_to_elide
+ @_boxes_to_elide ||=
+ panes_to_consider_for_eliding.reduce([]) do |array, pane|
+ array + (find_boxes_to_elide_within(pane) || [])
+ end
+ end
+
+ def panes_to_consider_for_eliding
+ panes.select { |pane| pane.type == :clean && pane.range.size > maximum }
+ end
+
+ def panes
+ @_panes ||=
+ BuildPanes.call(dirty_panes: padded_dirty_panes, lines: lines)
+ end
+
+ def padded_dirty_panes
+ @_padded_dirty_panes ||=
+ combine_congruent_panes(
+ dirty_panes
+ .map(&:padded)
+ .map { |pane| pane.capped_to(0, lines.size - 1) }
+ )
+ end
+
+ def dirty_panes
+ @_dirty_panes ||=
+ lines
+ .each_with_index
+ .select { |line, index| line.type != :noop }
+ .reduce([]) do |panes, (_, index)|
+ if !panes.empty? && panes.last.range.end == index - 1
+ panes[0..-2] + [panes[-1].extended_to(index)]
+ else
+ panes + [Pane.new(type: :dirty, range: index..index)]
+ end
+ end
+ end
+
+ def with_box_elided(box, lines)
+ box_at_start_of_lines =
+ if lines.first.complete_bookend?
+ box.range.begin == 1
+ else
+ box.range.begin == 0
+ end
+
+ box_at_end_of_lines =
+ if lines.last.complete_bookend?
+ box.range.end == lines.size - 2
+ else
+ box.range.end == lines.size - 1
+ end
+
+ if one_dimensional_line_tree? && outermost_box?(box)
+ if box_at_start_of_lines
+ with_start_of_box_elided(box, lines)
+ elsif box_at_end_of_lines
+ with_end_of_box_elided(box, lines)
+ else
+ with_middle_of_box_elided(box, lines)
+ end
+ else
+ with_subset_of_lines_elided(
+ lines,
+ range: box.range,
+ indentation_level: box.indentation_level
+ )
+ end
+ end
+
+ def outermost_box?(box)
+ box.indentation_level == all_indentation_levels.min
+ end
+
+ def one_dimensional_line_tree?
+ all_indentation_levels.size == 1
+ end
+
+ def all_indentation_levels
+ lines
+ .map(&:indentation_level)
+ .select { |indentation_level| indentation_level > 0 }
+ .uniq
+ end
+
+ def find_boxes_to_elide_within(pane)
+ set_of_boxes =
+ normalized_box_groups_at_decreasing_indentation_levels_within(pane)
+
+ total_size_before_eliding =
+ lines[pane.range].reject(&:complete_bookend?).size
+
+ if total_size_before_eliding > maximum
+ if maximum > 0
+ set_of_boxes.find do |boxes|
+ total_size_after_eliding =
+ total_size_before_eliding -
+ boxes.sum { |box| box.range.size - SIZE_OF_ELISION }
+ total_size_after_eliding <= maximum
+ end
+ else
+ set_of_boxes[-1]
+ end
+ else
+ []
+ end
+ end
+
+ def normalized_box_groups_at_decreasing_indentation_levels_within(pane)
+ box_groups_at_decreasing_indentation_levels_within(pane).map(
+ &method(:filter_out_boxes_fully_contained_in_others)
+ ).map(&method(:combine_congruent_boxes))
+ end
+
+ def box_groups_at_decreasing_indentation_levels_within(pane)
+ boxes_within_pane = boxes.select { |box| box.fits_fully_within?(pane) }
+
+ possible_indentation_levels =
+ boxes_within_pane
+ .map(&:indentation_level)
+ .select { |indentation_level| indentation_level > 0 }
+ .uniq
+ .sort
+ .reverse
+
+ possible_indentation_levels.map do |indentation_level|
+ boxes_within_pane.select do |box|
+ box.indentation_level >= indentation_level
+ end
+ end
+ end
+
+ def filter_out_boxes_fully_contained_in_others(boxes)
+ sorted_boxes =
+ boxes.sort_by do |box|
+ [box.indentation_level, box.range.begin, box.range.end]
+ end
+
+ boxes.reject do |box2|
+ sorted_boxes.any? do |box1|
+ !box1.equal?(box2) && box1.fully_contains?(box2)
+ end
+ end
+ end
+
+ def combine_congruent_boxes(boxes)
+ combine(boxes, on: :indentation_level)
+ end
+
+ def combine_congruent_panes(panes)
+ combine(panes, on: :type)
+ end
+
+ def combine(spannables, on:)
+ criterion = on
+ spannables.reduce([]) do |combined_spannables, spannable|
+ if (
+ !combined_spannables.empty? &&
+ spannable.range.begin <=
+ combined_spannables.last.range.end + 1 &&
+ spannable.public_send(criterion) ==
+ combined_spannables.last.public_send(criterion)
+ )
+ combined_spannables[0..-2] +
+ [combined_spannables[-1].extended_to(spannable.range.end)]
+ else
+ combined_spannables + [spannable]
+ end
+ end
+ end
+
+ def boxes
+ @_boxes ||= BuildBoxes.call(lines)
+ end
+
+ def with_start_of_box_elided(box, lines)
+ amount_to_elide =
+ if maximum > 0
+ box.range.size - maximum + SIZE_OF_ELISION
+ else
+ box.range.size
+ end
+
+ with_subset_of_lines_elided(
+ lines,
+ range:
+ Range.new(box.range.begin, box.range.begin + amount_to_elide - 1),
+ indentation_level: box.indentation_level
+ )
+ end
+
+ def with_end_of_box_elided(box, lines)
+ amount_to_elide =
+ if maximum > 0
+ box.range.size - maximum + SIZE_OF_ELISION
+ else
+ box.range.size
+ end
+
+ range =
+ if amount_to_elide > 0
+ Range.new(box.range.end - amount_to_elide + 1, box.range.end)
+ else
+ box.range
+ end
+
+ with_subset_of_lines_elided(
+ lines,
+ range: range,
+ indentation_level: box.indentation_level
+ )
+ end
+
+ def with_middle_of_box_elided(box, lines)
+ half_of_maximum, remainder =
+ if maximum > 0
+ (maximum - SIZE_OF_ELISION).divmod(2)
+ else
+ [0, 0]
+ end
+
+ opening_length, closing_length =
+ half_of_maximum,
+ half_of_maximum + remainder
+
+ with_subset_of_lines_elided(
+ lines,
+ range:
+ Range.new(
+ box.range.begin + opening_length,
+ box.range.end - closing_length
+ ),
+ indentation_level: box.indentation_level
+ )
+ end
+
+ def with_subset_of_lines_elided(lines, range:, indentation_level:)
+ with_slice_of_array_replaced(
+ lines,
+ range,
+ Elision.new(
+ indentation_level: indentation_level,
+ children: lines[range].map(&:as_elided)
+ )
+ )
+ end
+
+ def maximum
+ SuperDiff.configuration.diff_elision_maximum || 0
+ end
+
+ class BuildPanes
+ extend AttrExtras.mixin
+
+ method_object %i[dirty_panes! lines!]
+
+ def call
+ beginning + middle + ending
+ end
+
+ private
+
+ def beginning
+ if (dirty_panes.empty? || dirty_panes.first.range.begin == 0)
+ []
+ else
+ [
+ Pane.new(
+ type: :clean,
+ range: Range.new(0, dirty_panes.first.range.begin - 1)
+ )
+ ]
+ end
+ end
+
+ def middle
+ if dirty_panes.size == 1
+ dirty_panes
+ else
+ dirty_panes
+ .each_with_index
+ .each_cons(2)
+ .reduce([]) do |panes, ((pane1, _), (pane2, index2))|
+ panes +
+ [
+ pane1,
+ Pane.new(
+ type: :clean,
+ range:
+ Range.new(pane1.range.end + 1, pane2.range.begin - 1)
+ )
+ ] + (index2 == dirty_panes.size - 1 ? [pane2] : [])
+ end
+ end
+ end
+
+ def ending
+ if (
+ dirty_panes.empty? ||
+ dirty_panes.last.range.end >= lines.size - 1
+ )
+ []
+ else
+ [
+ Pane.new(
+ type: :clean,
+ range: Range.new(dirty_panes.last.range.end + 1, lines.size - 1)
+ )
+ ]
+ end
+ end
+ end
+
+ class Pane
+ extend AttrExtras.mixin
+
+ rattr_initialize %i[type! range!]
+
+ def extended_to(new_end)
+ self.class.new(type: type, range: range.begin..new_end)
+ end
+
+ def padded
+ self.class.new(type: type, range: Range.new(range.begin, range.end))
+ end
+
+ def capped_to(beginning, ending)
+ new_beginning = range.begin < beginning ? beginning : range.begin
+ new_ending = range.end > ending ? ending : range.end
+ self.class.new(
+ type: type,
+ range: Range.new(new_beginning, new_ending)
+ )
+ end
+ end
+
+ class BuildBoxes
+ def self.call(lines)
+ builder = new(lines)
+ builder.build
+ builder.final_boxes
+ end
+
+ attr_reader :final_boxes
+
+ def initialize(lines)
+ @lines = lines
+
+ @open_collection_boxes = []
+ @final_boxes = []
+ end
+
+ def build
+ lines.each_with_index do |line, index|
+ if line.opens_collection?
+ open_new_collection_box(line, index)
+ elsif line.closes_collection?
+ extend_working_collection_box(index)
+ close_working_collection_box
+ else
+ extend_working_collection_box(index) if open_collection_boxes.any?
+ record_item_box(line, index)
+ end
+ end
+ end
+
+ private
+
+ attr_reader :lines, :open_collection_boxes
+
+ def extend_working_collection_box(index)
+ open_collection_boxes.last.extend_to(index)
+ end
+
+ def close_working_collection_box
+ final_boxes << open_collection_boxes.pop
+ end
+
+ def open_new_collection_box(line, index)
+ open_collection_boxes << Box.new(
+ indentation_level: line.indentation_level,
+ range: index..index
+ )
+ end
+
+ def record_item_box(line, index)
+ final_boxes << Box.new(
+ indentation_level: line.indentation_level,
+ range: index..index
+ )
+ end
+ end
+
+ class Box
+ extend AttrExtras.mixin
+
+ rattr_initialize %i[indentation_level! range!]
+
+ def fully_contains?(other)
+ range.begin <= other.range.begin && range.end >= other.range.end
+ end
+
+ def fits_fully_within?(other)
+ other.range.begin <= range.begin && other.range.end >= range.end
+ end
+
+ def extended_to(new_end)
+ dup.tap { |clone| clone.extend_to(new_end) }
+ end
+
+ def extend_to(new_end)
+ @range = range.begin..new_end
+ end
+ end
+
+ class Elision
+ extend AttrExtras.mixin
+
+ rattr_initialize %i[indentation_level! children!]
+
+ def type
+ :elision
+ end
+
+ def prefix
+ ""
+ end
+
+ def value
+ "# ..."
+ end
+
+ def elided?
+ true
+ end
+
+ def add_comma?
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/core/tiered_lines_formatter.rb b/lib/super_diff/core/tiered_lines_formatter.rb
new file mode 100644
index 00000000..bf944374
--- /dev/null
+++ b/lib/super_diff/core/tiered_lines_formatter.rb
@@ -0,0 +1,77 @@
+module SuperDiff
+ module Core
+ class TieredLinesFormatter
+ extend AttrExtras.mixin
+
+ method_object :tiered_lines
+
+ def call
+ colorized_document.to_s.chomp
+ end
+
+ private
+
+ def colorized_document
+ Helpers.style do |doc|
+ formattable_lines.each do |formattable_line|
+ doc.public_send(
+ "#{formattable_line.color}_line",
+ formattable_line.content
+ )
+ end
+ end
+ end
+
+ def formattable_lines
+ tiered_lines.map { |line| FormattableLine.new(line) }
+ end
+
+ class FormattableLine
+ extend AttrExtras.mixin
+
+ INDENTATION_UNIT = " ".freeze
+ ICONS = { delete: "-", insert: "+", elision: " ", noop: " " }.freeze
+ COLORS = {
+ delete: :expected,
+ insert: :actual,
+ elision: :elision_marker,
+ noop: :plain
+ }.freeze
+
+ pattr_initialize :line
+
+ def content
+ icon + " " + indentation + line.prefix + line.value + possible_comma
+ end
+
+ def color
+ COLORS.fetch(line.type) do
+ raise(
+ KeyError,
+ "Couldn't find color for line type #{line.type.inspect}!"
+ )
+ end
+ end
+
+ private
+
+ def icon
+ ICONS.fetch(line.type) do
+ raise(
+ KeyError,
+ "Couldn't find icon for line type #{line.type.inspect}!"
+ )
+ end
+ end
+
+ def indentation
+ INDENTATION_UNIT * line.indentation_level
+ end
+
+ def possible_comma
+ line.add_comma? ? "," : ""
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/operations/unary_operation.rb b/lib/super_diff/core/unary_operation.rb
similarity index 88%
rename from lib/super_diff/operations/unary_operation.rb
rename to lib/super_diff/core/unary_operation.rb
index ad886d53..f6fc19bb 100644
--- a/lib/super_diff/operations/unary_operation.rb
+++ b/lib/super_diff/core/unary_operation.rb
@@ -1,5 +1,5 @@
module SuperDiff
- module Operations
+ module Core
class UnaryOperation
extend AttrExtras.mixin
diff --git a/lib/super_diff/diff_formatters/collection.rb b/lib/super_diff/diff_formatters/collection.rb
deleted file mode 100644
index e07b8949..00000000
--- a/lib/super_diff/diff_formatters/collection.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-module SuperDiff
- module DiffFormatters
- class Collection
- extend AttrExtras.mixin
-
- ICONS = { delete: "-", insert: "+" }.freeze
- STYLES = { insert: :actual, delete: :expected, noop: :plain }.freeze
-
- method_object(
- %i[
- open_token!
- close_token!
- operation_tree!
- indent_level!
- add_comma!
- collection_prefix!
- build_item_prefix!
- ]
- )
-
- def call
- lines.join("\n")
- end
-
- private
-
- attr_query :add_comma?
-
- def lines
- [
- " #{indentation}#{collection_prefix}#{open_token}",
- *contents,
- " #{indentation}#{close_token}#{comma}"
- ]
- end
-
- def contents
- operation_tree.map do |operation|
- if operation.name == :change
- handle_change_operation(operation)
- else
- handle_non_change_operation(operation)
- end
- end
- end
-
- def handle_change_operation(operation)
- SuperDiff::RecursionGuard.guarding_recursion_of(
- operation.left_collection,
- operation.right_collection
- ) do |already_seen|
- if already_seen
- raise "Infinite recursion!"
- else
- operation.child_operations.to_diff(
- indent_level: indent_level + 1,
- collection_prefix: build_item_prefix.call(operation),
- add_comma: operation.should_add_comma_after_displaying?
- )
- end
- end
- end
-
- def handle_non_change_operation(operation)
- icon = ICONS.fetch(operation.name, " ")
- style_name = STYLES.fetch(operation.name, :normal)
- chunk =
- build_chunk_for(
- operation,
- prefix: build_item_prefix.call(operation),
- icon: icon
- )
-
- chunk << "," if operation.should_add_comma_after_displaying?
-
- style_chunk(style_name, chunk)
- end
-
- def build_chunk_for(operation, prefix:, icon:)
- if operation.value.equal?(operation.collection)
- build_chunk_from_string(
- SuperDiff::RecursionGuard::PLACEHOLDER,
- prefix: build_item_prefix.call(operation),
- icon: icon
- )
- else
- build_chunk_by_inspecting(
- operation.value,
- prefix: build_item_prefix.call(operation),
- icon: icon
- )
- end
- end
-
- def build_chunk_by_inspecting(value, prefix:, icon:)
- inspection = SuperDiff.inspect_object(value, as_single_line: false)
- build_chunk_from_string(inspection, prefix: prefix, icon: icon)
- end
-
- def build_chunk_from_string(value, prefix:, icon:)
- value
- .split("\n")
- .map
- .with_index do |line, index|
- [
- icon,
- " ",
- indentation(offset: 1),
- (index == 0 ? prefix : ""),
- line
- ].join
- end
- .join("\n")
- end
-
- def style_chunk(style_name, chunk)
- chunk
- .split("\n")
- .map { |line| Helpers.style(style_name, line) }
- .join("\n")
- end
-
- def indentation(offset: 0)
- " " * (indent_level + offset)
- end
-
- def comma
- add_comma? ? "," : ""
- end
- end
- end
-end
diff --git a/lib/super_diff/diff_formatters/multiline_string.rb b/lib/super_diff/diff_formatters/multiline_string.rb
deleted file mode 100644
index b084bd9b..00000000
--- a/lib/super_diff/diff_formatters/multiline_string.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module SuperDiff
- module DiffFormatters
- class MultilineString < Base
- def self.applies_to?(operation_tree)
- operation_tree.is_a?(OperationTrees::MultilineString)
- end
-
- def call
- lines.join("\n")
- end
-
- private
-
- def lines
- operation_tree.reduce([]) do |array, operation|
- case operation.name
- when :change
- array << Helpers.style(:expected, "- #{operation.left_value}")
- array << Helpers.style(:actual, "+ #{operation.right_value}")
- when :delete
- array << Helpers.style(:expected, "- #{operation.value}")
- when :insert
- array << Helpers.style(:actual, "+ #{operation.value}")
- else
- array << Helpers.style(:plain, " #{operation.value}")
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/differs.rb b/lib/super_diff/differs.rb
index c2aba45f..d7a9f14e 100644
--- a/lib/super_diff/differs.rb
+++ b/lib/super_diff/differs.rb
@@ -1,16 +1,23 @@
module SuperDiff
module Differs
- autoload :Array, "super_diff/differs/array"
- autoload :Base, "super_diff/differs/base"
- autoload :CustomObject, "super_diff/differs/custom_object"
- autoload :DefaultObject, "super_diff/differs/default_object"
- autoload :Empty, "super_diff/differs/empty"
- autoload :Hash, "super_diff/differs/hash"
- autoload :Main, "super_diff/differs/main"
- autoload :MultilineString, "super_diff/differs/multiline_string"
- autoload :TimeLike, "super_diff/differs/time_like"
- autoload :DateLike, "super_diff/differs/date_like"
+ def self.const_missing(missing_const_name)
+ if missing_const_name == :Base
+ warn <<~EOT
+ WARNING: SuperDiff::Differs::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::AbstractDiffer instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core::AbstractDiffer
+ elsif Basic::Differs.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::Differs::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Basic::Differs::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Basic::Differs.const_get(missing_const_name)
+ else
+ super
+ end
+ end
end
end
-
-require "super_diff/differs/defaults"
diff --git a/lib/super_diff/differs/array.rb b/lib/super_diff/differs/array.rb
deleted file mode 100644
index 8fc4181c..00000000
--- a/lib/super_diff/differs/array.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module Differs
- class Array < Base
- def self.applies_to?(expected, actual)
- expected.is_a?(::Array) && actual.is_a?(::Array)
- end
-
- protected
-
- def operation_tree_builder_class
- OperationTreeBuilders::Array
- end
- end
- end
-end
diff --git a/lib/super_diff/differs/custom_object.rb b/lib/super_diff/differs/custom_object.rb
deleted file mode 100644
index 3d4a9c75..00000000
--- a/lib/super_diff/differs/custom_object.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module SuperDiff
- module Differs
- class CustomObject < Base
- def self.applies_to?(expected, actual)
- expected.class == actual.class &&
- expected.respond_to?(:attributes_for_super_diff) &&
- actual.respond_to?(:attributes_for_super_diff)
- end
-
- protected
-
- def operation_tree_builder_class
- OperationTreeBuilders::CustomObject
- end
- end
- end
-end
diff --git a/lib/super_diff/differs/date_like.rb b/lib/super_diff/differs/date_like.rb
deleted file mode 100644
index a30f0030..00000000
--- a/lib/super_diff/differs/date_like.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module Differs
- class DateLike < Base
- def self.applies_to?(expected, actual)
- SuperDiff.date_like?(expected) && SuperDiff.date_like?(actual)
- end
-
- protected
-
- def operation_tree_builder_class
- OperationTreeBuilders::DateLike
- end
- end
- end
-end
diff --git a/lib/super_diff/differs/default_object.rb b/lib/super_diff/differs/default_object.rb
deleted file mode 100644
index 784a960b..00000000
--- a/lib/super_diff/differs/default_object.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module SuperDiff
- module Differs
- class DefaultObject < Base
- def self.applies_to?(expected, actual)
- expected.class == actual.class
- end
-
- protected
-
- def operation_tree
- OperationTreeBuilders::Main.call(
- expected: expected,
- actual: actual,
- all_or_nothing: true
- )
- end
- end
- end
-end
diff --git a/lib/super_diff/differs/defaults.rb b/lib/super_diff/differs/defaults.rb
deleted file mode 100644
index d1e58113..00000000
--- a/lib/super_diff/differs/defaults.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module SuperDiff
- module Differs
- DEFAULTS = [
- Array,
- Hash,
- TimeLike,
- DateLike,
- MultilineString,
- CustomObject,
- DefaultObject
- ].freeze
- end
-end
diff --git a/lib/super_diff/differs/empty.rb b/lib/super_diff/differs/empty.rb
deleted file mode 100644
index ae20d779..00000000
--- a/lib/super_diff/differs/empty.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module SuperDiff
- module Differs
- class Empty < Base
- def self.applies_to?(_expected, _actual)
- true
- end
-
- def call
- ""
- end
- end
- end
-end
diff --git a/lib/super_diff/differs/hash.rb b/lib/super_diff/differs/hash.rb
deleted file mode 100644
index 215b4139..00000000
--- a/lib/super_diff/differs/hash.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module Differs
- class Hash < Base
- def self.applies_to?(expected, actual)
- expected.is_a?(::Hash) && actual.is_a?(::Hash)
- end
-
- protected
-
- def operation_tree_builder_class
- OperationTreeBuilders::Hash
- end
- end
- end
-end
diff --git a/lib/super_diff/differs/main.rb b/lib/super_diff/differs/main.rb
deleted file mode 100644
index 14a086e9..00000000
--- a/lib/super_diff/differs/main.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module SuperDiff
- module Differs
- class Main
- extend AttrExtras.mixin
-
- method_object(:expected, :actual, [indent_level: 0, omit_empty: false])
-
- def call
- if resolved_class
- resolved_class.call(expected, actual, indent_level: indent_level)
- else
- raise Errors::NoDifferAvailableError.create(expected, actual)
- end
- end
-
- private
-
- attr_query :omit_empty?
-
- def resolved_class
- available_classes.find { |klass| klass.applies_to?(expected, actual) }
- end
-
- def available_classes
- classes = SuperDiff.configuration.extra_differ_classes + DEFAULTS
-
- omit_empty? ? classes : classes + [Empty]
- end
- end
- end
-end
diff --git a/lib/super_diff/differs/multiline_string.rb b/lib/super_diff/differs/multiline_string.rb
deleted file mode 100644
index b8b6c554..00000000
--- a/lib/super_diff/differs/multiline_string.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module SuperDiff
- module Differs
- class MultilineString < Base
- def self.applies_to?(expected, actual)
- expected.is_a?(::String) && actual.is_a?(::String) &&
- (expected.include?("\n") || actual.include?("\n"))
- end
-
- protected
-
- def operation_tree_builder_class
- OperationTreeBuilders::MultilineString
- end
- end
- end
-end
diff --git a/lib/super_diff/differs/time_like.rb b/lib/super_diff/differs/time_like.rb
deleted file mode 100644
index dded8cff..00000000
--- a/lib/super_diff/differs/time_like.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module Differs
- class TimeLike < Base
- def self.applies_to?(expected, actual)
- SuperDiff.time_like?(expected) && SuperDiff.time_like?(actual)
- end
-
- protected
-
- def operation_tree_builder_class
- OperationTreeBuilders::TimeLike
- end
- end
- end
-end
diff --git a/lib/super_diff/equality_matchers/array.rb b/lib/super_diff/equality_matchers/array.rb
index 0af10073..f2012ef6 100644
--- a/lib/super_diff/equality_matchers/array.rb
+++ b/lib/super_diff/equality_matchers/array.rb
@@ -10,13 +10,13 @@ def fail
Differing arrays.
#{
- Helpers.style(
+ Core::Helpers.style(
:expected,
"Expected: " + SuperDiff.inspect_object(expected, as_lines: false)
)
}
#{
- Helpers.style(
+ Core::Helpers.style(
:actual,
" Actual: " + SuperDiff.inspect_object(actual, as_lines: false)
)
@@ -31,7 +31,7 @@ def fail
protected
def diff
- Differs::Array.call(expected, actual, indent_level: 0)
+ Basic::Differs::Array.call(expected, actual, indent_level: 0)
end
end
end
diff --git a/lib/super_diff/equality_matchers/default.rb b/lib/super_diff/equality_matchers/default.rb
index d00ffce0..3fcabfb7 100644
--- a/lib/super_diff/equality_matchers/default.rb
+++ b/lib/super_diff/equality_matchers/default.rb
@@ -18,14 +18,14 @@ def fail
protected
def expected_line
- Helpers.style(
+ Core::Helpers.style(
:expected,
"Expected: " + SuperDiff.inspect_object(expected, as_lines: false)
)
end
def actual_line
- Helpers.style(
+ Core::Helpers.style(
:actual,
" Actual: " + SuperDiff.inspect_object(actual, as_lines: false)
)
@@ -45,7 +45,12 @@ def diff_section
end
def diff
- Differs::Main.call(expected, actual, indent_level: 0)
+ SuperDiff.diff(
+ expected,
+ actual,
+ indent_level: 0,
+ raise_if_nothing_applies: false
+ )
end
end
end
diff --git a/lib/super_diff/equality_matchers/hash.rb b/lib/super_diff/equality_matchers/hash.rb
index 5fbe5335..8cd57b1c 100644
--- a/lib/super_diff/equality_matchers/hash.rb
+++ b/lib/super_diff/equality_matchers/hash.rb
@@ -10,13 +10,13 @@ def fail
Differing hashes.
#{
- Helpers.style(
+ Core::Helpers.style(
:expected,
"Expected: " + SuperDiff.inspect_object(expected, as_lines: false)
)
}
#{
- Helpers.style(
+ Core::Helpers.style(
:actual,
" Actual: " + SuperDiff.inspect_object(actual, as_lines: false)
)
@@ -31,7 +31,7 @@ def fail
protected
def diff
- Differs::Hash.call(expected, actual, indent_level: 0)
+ Basic::Differs::Hash.call(expected, actual, indent_level: 0)
end
end
end
diff --git a/lib/super_diff/equality_matchers/multiline_string.rb b/lib/super_diff/equality_matchers/multiline_string.rb
index b7d1da8d..5ed60689 100644
--- a/lib/super_diff/equality_matchers/multiline_string.rb
+++ b/lib/super_diff/equality_matchers/multiline_string.rb
@@ -11,13 +11,13 @@ def fail
#{
# TODO: This whole thing should not be red or green, just the values
- Helpers.style(
+ Core::Helpers.style(
:expected,
"Expected: " + SuperDiff.inspect_object(expected, as_lines: false)
)
}
#{
- Helpers.style(
+ Core::Helpers.style(
:actual,
" Actual: " + SuperDiff.inspect_object(actual, as_lines: false)
)
@@ -32,7 +32,7 @@ def fail
private
def diff
- Differs::MultilineString.call(expected, actual, indent_level: 0)
+ Basic::Differs::MultilineString.call(expected, actual, indent_level: 0)
end
end
end
diff --git a/lib/super_diff/equality_matchers/primitive.rb b/lib/super_diff/equality_matchers/primitive.rb
index a0a1315b..c0ce6299 100644
--- a/lib/super_diff/equality_matchers/primitive.rb
+++ b/lib/super_diff/equality_matchers/primitive.rb
@@ -8,16 +8,16 @@ def self.applies_to?(value)
def fail
<<~OUTPUT.strip
- Differing #{Helpers.plural_type_for(actual)}.
+ Differing #{Core::Helpers.plural_type_for(actual)}.
#{
- Helpers.style(
+ Core::Helpers.style(
:expected,
"Expected: " + SuperDiff.inspect_object(expected, as_lines: false)
)
}
#{
- Helpers.style(
+ Core::Helpers.style(
:actual,
" Actual: " + SuperDiff.inspect_object(actual, as_lines: false)
)
diff --git a/lib/super_diff/equality_matchers/singleline_string.rb b/lib/super_diff/equality_matchers/singleline_string.rb
index d658781e..7df1bb25 100644
--- a/lib/super_diff/equality_matchers/singleline_string.rb
+++ b/lib/super_diff/equality_matchers/singleline_string.rb
@@ -10,13 +10,13 @@ def fail
Differing strings.
#{
- Helpers.style(
+ Core::Helpers.style(
:expected,
"Expected: " + SuperDiff.inspect_object(expected, as_lines: false)
)
}
#{
- Helpers.style(
+ Core::Helpers.style(
:actual,
" Actual: " + SuperDiff.inspect_object(actual, as_lines: false)
)
diff --git a/lib/super_diff/errors.rb b/lib/super_diff/errors.rb
index 1ff0759a..fe991e57 100644
--- a/lib/super_diff/errors.rb
+++ b/lib/super_diff/errors.rb
@@ -1,12 +1,16 @@
module SuperDiff
module Errors
- autoload(
- :NoDifferAvailableError,
- "super_diff/errors/no_differ_available_error"
- )
- autoload(
- :NoOperationTreeBuilderAvailableError,
- "super_diff/errors/no_operation_tree_builder_available_error"
- )
+ def self.const_missing(missing_const_name)
+ if Core.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::Errors::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core.const_get(missing_const_name)
+ else
+ super
+ end
+ end
end
end
diff --git a/lib/super_diff/gem_version.rb b/lib/super_diff/gem_version.rb
deleted file mode 100644
index e14a5672..00000000
--- a/lib/super_diff/gem_version.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module SuperDiff
- class GemVersion
- def initialize(version)
- @version = Gem::Version.new(version.to_s)
- end
-
- def <(other)
- compare?(:<, other)
- end
-
- def <=(other)
- compare?(:<=, other)
- end
-
- def ==(other)
- compare?(:==, other)
- end
-
- def >=(other)
- compare?(:>=, other)
- end
-
- def >(other)
- compare?(:>, other)
- end
-
- def =~(other)
- Gem::Requirement.new(other).satisfied_by?(version)
- end
-
- def to_s
- version.to_s
- end
-
- private
-
- attr_reader :version
-
- def compare?(operator, other_version)
- Gem::Requirement.new("#{operator} #{other_version}").satisfied_by?(
- version
- )
- end
- end
-end
diff --git a/lib/super_diff/helpers.rb b/lib/super_diff/helpers.rb
deleted file mode 100644
index da07c6d4..00000000
--- a/lib/super_diff/helpers.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-module SuperDiff
- module Helpers
- extend self
-
- # TODO: Simplify this
- def style(*args, color_enabled: true, **opts, &block)
- klass =
- if color_enabled && Csi.color_enabled?
- Csi::ColorizedDocument
- else
- Csi::UncolorizedDocument
- end
-
- document = klass.new.extend(ColorizedDocumentExtensions)
-
- if block
- document.__send__(:evaluate_block, &block)
- else
- document.colorize(*args, **opts)
- end
-
- document
- end
-
- def plural_type_for(value)
- case value
- when Numeric
- "numbers"
- when String
- "strings"
- when Symbol
- "symbols"
- else
- "objects"
- end
- end
-
- def jruby?
- defined?(JRUBY_VERSION)
- end
-
- def ruby_version_matches?(version_string)
- Gem::Requirement.new(version_string).satisfied_by?(
- Gem::Version.new(RUBY_VERSION)
- )
- end
-
- if jruby?
- def object_address_for(object)
- # Source:
- "0x%x" % object.hash
- end
- elsif ruby_version_matches?(">= 2.7.0")
- require "json"
- require "objspace"
-
- def object_address_for(object)
- # Sources: and
- json = JSON.parse(ObjectSpace.dump(object))
- json.is_a?(Hash) ? "0x%016x" % Integer(json["address"], 16) : ""
- end
- else
- def object_address_for(object)
- "0x%016x" % (object.object_id * 2)
- end
- end
-
- def with_slice_of_array_replaced(array, range, replacement)
- beginning =
- if range.begin > 0
- array[Range.new(0, range.begin - 1)]
- else
- []
- end
-
- ending =
- if range.end <= array.length - 1
- array[Range.new(range.end + 1, array.length - 1)]
- else
- []
- end
-
- beginning + [replacement] + ending
- end
- end
-end
diff --git a/lib/super_diff/implementation_checks.rb b/lib/super_diff/implementation_checks.rb
deleted file mode 100644
index e4992227..00000000
--- a/lib/super_diff/implementation_checks.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module SuperDiff
- module ImplementationChecks
- protected def unimplemented_instance_method!
- raise(
- NotImplementedError,
- "#{self.class} must implement ##{caller_locations(1, 1).first.label}",
- caller(1)
- )
- end
-
- protected def unimplemented_class_method!
- raise(
- NotImplementedError,
- "#{self} must implement .#{caller_locations(1, 1).first.label}",
- caller(1)
- )
- end
- end
-end
diff --git a/lib/super_diff/line.rb b/lib/super_diff/line.rb
deleted file mode 100644
index de6d6eae..00000000
--- a/lib/super_diff/line.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-module SuperDiff
- class Line
- extend AttrExtras.mixin
-
- ICONS = { delete: "-", insert: "+", noop: " " }.freeze
- COLORS = { insert: :actual, delete: :expected, noop: :plain }.freeze
-
- rattr_initialize(
- [
- :type!,
- :indentation_level!,
- :value!,
- prefix: "",
- add_comma: false,
- children: [],
- elided: false,
- collection_bookend: nil,
- complete_bookend: nil
- ]
- )
- attr_query :add_comma?
- attr_query :elided?
-
- def clone_with(overrides = {})
- self.class.new(
- type: type,
- indentation_level: indentation_level,
- prefix: prefix,
- value: value,
- add_comma: add_comma?,
- children: children,
- elided: elided?,
- collection_bookend: collection_bookend,
- complete_bookend: complete_bookend,
- **overrides
- )
- end
-
- def icon
- ICONS.fetch(type)
- end
-
- def color
- COLORS.fetch(type)
- end
-
- def with_comma
- clone_with(add_comma: true)
- end
-
- def as_elided
- clone_with(elided: true)
- end
-
- def with_value_prepended(prelude)
- clone_with(value: prelude + value)
- end
-
- def with_value_appended(suffix)
- clone_with(value: value + suffix)
- end
-
- def prefixed_with(prefix)
- clone_with(prefix: prefix + self.prefix)
- end
-
- def with_complete_bookend(complete_bookend)
- clone_with(complete_bookend: complete_bookend)
- end
-
- def opens_collection?
- collection_bookend == :open
- end
-
- def closes_collection?
- collection_bookend == :close
- end
-
- def complete_bookend?
- complete_bookend != nil
- end
- end
-end
diff --git a/lib/super_diff/object_inspection.rb b/lib/super_diff/object_inspection.rb
index 961a9faf..cccdfee5 100644
--- a/lib/super_diff/object_inspection.rb
+++ b/lib/super_diff/object_inspection.rb
@@ -1,18 +1,67 @@
module SuperDiff
module ObjectInspection
- autoload :InspectionTree, "super_diff/object_inspection/inspection_tree"
- autoload(
- :InspectionTreeBuilders,
- "super_diff/object_inspection/inspection_tree_builders"
- )
- autoload :Nodes, "super_diff/object_inspection/nodes"
- autoload(
- :PrefixForNextNode,
- "super_diff/object_inspection/prefix_for_next_node"
- )
- autoload(
- :PreludeForNextNode,
- "super_diff/object_inspection/prelude_for_next_node"
- )
+ module InspectionTreeBuilders
+ def self.const_missing(missing_const_name)
+ if missing_const_name == :Base
+ warn <<~EOT
+ WARNING: SuperDiff::ObjectInspection::InspectionTreeBuilders::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::AbstractInspectionTreeBuilder instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core::AbstractInspectionTreeBuilder
+ elsif Basic::InspectionTreeBuilders.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::ObjectInspection::InspectionTreeBuilders::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Basic::InspectionTreeBuilders::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Basic::InspectionTreeBuilders.const_get(missing_const_name)
+ else
+ super
+ end
+ end
+ end
+
+ module Nodes
+ def self.const_missing(missing_const_name)
+ if Core::InspectionTreeNodes.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::ObjectInspection::Nodes::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::InspectionTreeNodes::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core::InspectionTreeNodes.const_get(missing_const_name)
+ else
+ super
+ end
+ end
+ end
+
+ def self.const_missing(missing_const_name)
+ if missing_const_name == :PrefixForNextNode
+ warn <<~EOT
+ WARNING: SuperDiff::ObjectInspection::PrefixForNextNode is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::PrefixForNextInspectionTreeNode instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core::PrefixForNextInspectionTreeNode
+ elsif missing_const_name == :PreludeForNextNode
+ warn <<~EOT
+ WARNING: SuperDiff::ObjectInspection::PreludeForNextNode is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::PreludeForNextInspectionTreeNode instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core::PreludeForNextInspectionTreeNode
+ elsif Core.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::ObjectInspection::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core.const_get(missing_const_name)
+ else
+ super
+ end
+ end
end
end
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders.rb b/lib/super_diff/object_inspection/inspection_tree_builders.rb
deleted file mode 100644
index 5da89908..00000000
--- a/lib/super_diff/object_inspection/inspection_tree_builders.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module SuperDiff
- module ObjectInspection
- module InspectionTreeBuilders
- autoload(
- :Base,
- "super_diff/object_inspection/inspection_tree_builders/base"
- )
- autoload(
- :Array,
- "super_diff/object_inspection/inspection_tree_builders/array"
- )
- autoload(
- :CustomObject,
- "super_diff/object_inspection/inspection_tree_builders/custom_object"
- )
- autoload(
- :DefaultObject,
- "super_diff/object_inspection/inspection_tree_builders/default_object"
- )
- autoload(
- :Hash,
- "super_diff/object_inspection/inspection_tree_builders/hash"
- )
- autoload(
- :Main,
- "super_diff/object_inspection/inspection_tree_builders/main"
- )
- autoload(
- :Primitive,
- "super_diff/object_inspection/inspection_tree_builders/primitive"
- )
- autoload(
- :String,
- "super_diff/object_inspection/inspection_tree_builders/string"
- )
- autoload(
- :TimeLike,
- "super_diff/object_inspection/inspection_tree_builders/time_like"
- )
- autoload(
- :DateLike,
- "super_diff/object_inspection/inspection_tree_builders/date_like"
- )
- end
- end
-end
-
-require "super_diff/object_inspection/inspection_tree_builders/defaults"
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/base.rb b/lib/super_diff/object_inspection/inspection_tree_builders/base.rb
deleted file mode 100644
index aaf40d9a..00000000
--- a/lib/super_diff/object_inspection/inspection_tree_builders/base.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module SuperDiff
- module ObjectInspection
- module InspectionTreeBuilders
- class Base
- extend AttrExtras.mixin
- extend ImplementationChecks
- include ImplementationChecks
-
- def self.applies_to?(_value)
- unimplemented_class_method!
- end
-
- method_object :object
-
- def call
- unimplemented_instance_method!
- end
-
- protected
-
- def inspection_tree
- unimplemented_instance_method!
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/defaults.rb b/lib/super_diff/object_inspection/inspection_tree_builders/defaults.rb
deleted file mode 100644
index 53226565..00000000
--- a/lib/super_diff/object_inspection/inspection_tree_builders/defaults.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module ObjectInspection
- module InspectionTreeBuilders
- DEFAULTS = [
- CustomObject,
- Array,
- Hash,
- Primitive,
- TimeLike,
- DateLike,
- DefaultObject
- ].freeze
- end
- end
-end
diff --git a/lib/super_diff/object_inspection/inspection_tree_builders/main.rb b/lib/super_diff/object_inspection/inspection_tree_builders/main.rb
deleted file mode 100644
index cc395adf..00000000
--- a/lib/super_diff/object_inspection/inspection_tree_builders/main.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module SuperDiff
- module ObjectInspection
- module InspectionTreeBuilders
- class Main
- extend AttrExtras.mixin
-
- method_object :object
-
- def call
- if resolved_class
- resolved_class.call(object)
- else
- raise NoInspectorAvailableError.create(object)
- end
- end
-
- private
-
- def resolved_class
- available_classes.find { |klass| klass.applies_to?(object) }
- end
-
- def available_classes
- SuperDiff.configuration.extra_inspection_tree_builder_classes +
- DEFAULTS
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/object_inspection/nodes.rb b/lib/super_diff/object_inspection/nodes.rb
deleted file mode 100644
index 8d737b9a..00000000
--- a/lib/super_diff/object_inspection/nodes.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module SuperDiff
- module ObjectInspection
- module Nodes
- autoload(
- :AsLinesWhenRenderingToLines,
- "super_diff/object_inspection/nodes/as_lines_when_rendering_to_lines"
- )
- autoload(
- :AsPrefixWhenRenderingToLines,
- "super_diff/object_inspection/nodes/as_prefix_when_rendering_to_lines"
- )
- autoload(
- :AsPreludeWhenRenderingToLines,
- "super_diff/object_inspection/nodes/as_prelude_when_rendering_to_lines"
- )
- autoload(
- :AsSingleLine,
- "super_diff/object_inspection/nodes/as_single_line"
- )
- autoload :Base, "super_diff/object_inspection/nodes/base"
- autoload :Inspection, "super_diff/object_inspection/nodes/inspection"
- autoload :Nesting, "super_diff/object_inspection/nodes/nesting"
- autoload :OnlyWhen, "super_diff/object_inspection/nodes/only_when"
- autoload :Text, "super_diff/object_inspection/nodes/text"
- autoload(
- :WhenRenderingToLines,
- "super_diff/object_inspection/nodes/when_rendering_to_lines"
- )
- autoload(
- :WhenRenderingToString,
- "super_diff/object_inspection/nodes/when_rendering_to_string"
- )
-
- def self.registry
- @_registry ||= [
- AsLinesWhenRenderingToLines,
- AsPrefixWhenRenderingToLines,
- AsPreludeWhenRenderingToLines,
- AsSingleLine,
- Inspection,
- Nesting,
- OnlyWhen,
- Text,
- WhenRenderingToLines,
- WhenRenderingToString
- ]
- end
- end
- end
-end
diff --git a/lib/super_diff/object_inspection/prefix_for_next_node.rb b/lib/super_diff/object_inspection/prefix_for_next_node.rb
deleted file mode 100644
index baddf1ba..00000000
--- a/lib/super_diff/object_inspection/prefix_for_next_node.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module SuperDiff
- module ObjectInspection
- class PrefixForNextNode < String
- end
- end
-end
diff --git a/lib/super_diff/object_inspection/prelude_for_next_node.rb b/lib/super_diff/object_inspection/prelude_for_next_node.rb
deleted file mode 100644
index 037a04a7..00000000
--- a/lib/super_diff/object_inspection/prelude_for_next_node.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module SuperDiff
- module ObjectInspection
- class PreludeForNextNode < String
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_builders.rb b/lib/super_diff/operation_tree_builders.rb
index af8b0c56..5fd1bd03 100644
--- a/lib/super_diff/operation_tree_builders.rb
+++ b/lib/super_diff/operation_tree_builders.rb
@@ -1,19 +1,23 @@
module SuperDiff
module OperationTreeBuilders
- autoload :Array, "super_diff/operation_tree_builders/array"
- autoload :Base, "super_diff/operation_tree_builders/base"
- autoload :CustomObject, "super_diff/operation_tree_builders/custom_object"
- autoload :DefaultObject, "super_diff/operation_tree_builders/default_object"
- autoload :Hash, "super_diff/operation_tree_builders/hash"
- autoload :Main, "super_diff/operation_tree_builders/main"
- # TODO: Where is this used?
- autoload(
- :MultilineString,
- "super_diff/operation_tree_builders/multiline_string"
- )
- autoload :TimeLike, "super_diff/operation_tree_builders/time_like"
- autoload :DateLike, "super_diff/operation_tree_builders/date_like"
+ def self.const_missing(missing_const_name)
+ if missing_const_name == :Base
+ warn <<~EOT
+ WARNING: SuperDiff::OperationTreeBuilders::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::AbstractOperationTreeBuilder instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core::AbstractOperationTreeBuilder
+ elsif Basic::OperationTreeBuilders.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::OperationTreeBuilders::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Basic::OperationTreeBuilders::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Basic::OperationTreeBuilders.const_get(missing_const_name)
+ else
+ super
+ end
+ end
end
end
-
-require "super_diff/operation_tree_builders/defaults"
diff --git a/lib/super_diff/operation_tree_builders/array.rb b/lib/super_diff/operation_tree_builders/array.rb
deleted file mode 100644
index 3c46e374..00000000
--- a/lib/super_diff/operation_tree_builders/array.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-module SuperDiff
- module OperationTreeBuilders
- class Array < Base
- def self.applies_to?(expected, actual)
- expected.is_a?(::Array) && actual.is_a?(::Array)
- end
-
- def call
- Diff::LCS.traverse_balanced(expected, actual, lcs_callbacks)
- operation_tree
- end
-
- private
-
- def lcs_callbacks
- @_lcs_callbacks ||=
- LcsCallbacks.new(
- operation_tree: operation_tree,
- expected: expected,
- actual: actual,
- compare: method(:compare)
- )
- end
-
- def operation_tree
- @_operation_tree ||= OperationTrees::Array.new([])
- end
-
- class LcsCallbacks
- extend AttrExtras.mixin
-
- pattr_initialize %i[operation_tree! expected! actual! compare!]
- public :operation_tree
-
- def match(event)
- add_noop_operation(event)
- end
-
- def discard_a(event)
- add_delete_operation(event)
- end
-
- def discard_b(event)
- add_insert_operation(event)
- end
-
- def change(event)
- children = compare.(event.old_element, event.new_element)
-
- if children
- add_change_operation(event, children)
- else
- add_delete_operation(event)
- add_insert_operation(event)
- end
- end
-
- private
-
- def add_delete_operation(event)
- operation_tree << Operations::UnaryOperation.new(
- name: :delete,
- collection: expected,
- key: event.old_position,
- value: event.old_element,
- index: event.old_position
- )
- end
-
- def add_insert_operation(event)
- operation_tree << Operations::UnaryOperation.new(
- name: :insert,
- collection: actual,
- key: event.new_position,
- value: event.new_element,
- index: event.new_position
- )
- end
-
- def add_noop_operation(event)
- operation_tree << Operations::UnaryOperation.new(
- name: :noop,
- collection: actual,
- key: event.new_position,
- value: event.new_element,
- index: event.new_position
- )
- end
-
- def add_change_operation(event, children)
- operation_tree << Operations::BinaryOperation.new(
- name: :change,
- left_collection: expected,
- right_collection: actual,
- left_key: event.old_position,
- right_key: event.new_position,
- left_value: event.old_element,
- right_value: event.new_element,
- left_index: event.old_position,
- right_index: event.new_position,
- children: children
- )
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_builders/custom_object.rb b/lib/super_diff/operation_tree_builders/custom_object.rb
deleted file mode 100644
index 6f70a701..00000000
--- a/lib/super_diff/operation_tree_builders/custom_object.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-module SuperDiff
- module OperationTreeBuilders
- class CustomObject < DefaultObject
- def self.applies_to?(expected, actual)
- expected.class == actual.class &&
- expected.respond_to?(:attributes_for_super_diff) &&
- actual.respond_to?(:attributes_for_super_diff)
- end
-
- protected
-
- def build_operation_tree
- # NOTE: It doesn't matter whether we use expected or actual here,
- # because all we care about is the name of the class
- OperationTrees::CustomObject.new([], underlying_object: actual)
- end
-
- def attribute_names
- expected.attributes_for_super_diff.keys &
- actual.attributes_for_super_diff.keys
- end
-
- private
-
- attr_reader :expected_attributes, :actual_attributes
-
- def establish_expected_and_actual_attributes
- @expected_attributes =
- attribute_names.reduce({}) do |hash, name|
- hash.merge(name => expected.public_send(name))
- end
-
- @actual_attributes =
- attribute_names.reduce({}) do |hash, name|
- hash.merge(name => actual.public_send(name))
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_builders/date_like.rb b/lib/super_diff/operation_tree_builders/date_like.rb
deleted file mode 100644
index 3dc091e1..00000000
--- a/lib/super_diff/operation_tree_builders/date_like.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module OperationTreeBuilders
- class DateLike < CustomObject
- def self.applies_to?(expected, actual)
- SuperDiff.date_like?(expected) && SuperDiff.date_like?(actual)
- end
-
- protected
-
- def attribute_names
- %w[year month day]
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_builders/default_object.rb b/lib/super_diff/operation_tree_builders/default_object.rb
deleted file mode 100644
index 326ec73b..00000000
--- a/lib/super_diff/operation_tree_builders/default_object.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-module SuperDiff
- module OperationTreeBuilders
- class DefaultObject < Base
- def self.applies_to?(_expected, _actual)
- true
- end
-
- def initialize(*args)
- super(*args)
-
- establish_expected_and_actual_attributes
- end
-
- protected
-
- def unary_operations
- attribute_names.reduce([]) do |operations, name|
- possibly_add_noop_operation_to(operations, name)
- possibly_add_delete_operation_to(operations, name)
- possibly_add_insert_operation_to(operations, name)
- operations
- end
- end
-
- def build_operation_tree
- # XXX This assumes that `expected` and `actual` are the same
- # TODO: Does this need to be find_operation_tree_for?
- OperationTrees::DefaultObject.new([], underlying_object: actual)
- end
-
- def find_operation_tree_for(value)
- OperationTrees::Main.call(value)
- end
-
- def attribute_names
- (
- expected.instance_variables.sort & actual.instance_variables.sort
- ).map { |variable_name| variable_name[1..-1] }
- end
-
- private
-
- attr_reader :expected_attributes, :actual_attributes
-
- def establish_expected_and_actual_attributes
- @expected_attributes =
- attribute_names.reduce({}) do |hash, name|
- hash.merge(name => expected.instance_variable_get("@#{name}"))
- end
-
- @actual_attributes =
- attribute_names.reduce({}) do |hash, name|
- hash.merge(name => actual.instance_variable_get("@#{name}"))
- end
- end
-
- def possibly_add_noop_operation_to(operations, attribute_name)
- if should_add_noop_operation?(attribute_name)
- operations << Operations::UnaryOperation.new(
- name: :noop,
- collection: actual_attributes,
- key: attribute_name,
- index: attribute_names.index(attribute_name),
- value: actual_attributes[attribute_name]
- )
- end
- end
-
- def should_add_noop_operation?(attribute_name)
- expected_attributes.include?(attribute_name) &&
- actual_attributes.include?(attribute_name) &&
- expected_attributes[attribute_name] ==
- actual_attributes[attribute_name]
- end
-
- def possibly_add_delete_operation_to(operations, attribute_name)
- if should_add_delete_operation?(attribute_name)
- operations << Operations::UnaryOperation.new(
- name: :delete,
- collection: expected_attributes,
- key: attribute_name,
- index: attribute_names.index(attribute_name),
- value: expected_attributes[attribute_name]
- )
- end
- end
-
- def should_add_delete_operation?(attribute_name)
- expected_attributes.include?(attribute_name) &&
- (
- !actual_attributes.include?(attribute_name) ||
- expected_attributes[attribute_name] !=
- actual_attributes[attribute_name]
- )
- end
-
- def possibly_add_insert_operation_to(operations, attribute_name)
- if should_add_insert_operation?(attribute_name)
- operations << Operations::UnaryOperation.new(
- name: :insert,
- collection: actual_attributes,
- key: attribute_name,
- index: attribute_names.index(attribute_name),
- value: actual_attributes[attribute_name]
- )
- end
- end
-
- def should_add_insert_operation?(attribute_name)
- !expected_attributes.include?(attribute_name) ||
- (
- actual_attributes.include?(attribute_name) &&
- expected_attributes[attribute_name] !=
- actual_attributes[attribute_name]
- )
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_builders/defaults.rb b/lib/super_diff/operation_tree_builders/defaults.rb
deleted file mode 100644
index e3c3d797..00000000
--- a/lib/super_diff/operation_tree_builders/defaults.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module SuperDiff
- module OperationTreeBuilders
- DEFAULTS = [Array, Hash, TimeLike, DateLike, CustomObject].freeze
- end
-end
diff --git a/lib/super_diff/operation_tree_builders/hash.rb b/lib/super_diff/operation_tree_builders/hash.rb
deleted file mode 100644
index 56d665b0..00000000
--- a/lib/super_diff/operation_tree_builders/hash.rb
+++ /dev/null
@@ -1,218 +0,0 @@
-module SuperDiff
- module OperationTreeBuilders
- class Hash < Base
- def self.applies_to?(expected, actual)
- expected.is_a?(::Hash) && actual.is_a?(::Hash)
- end
-
- protected
-
- def unary_operations
- unary_operations_using_variant_of_patience_algorithm
- end
-
- def build_operation_tree
- OperationTrees::Hash.new([])
- end
-
- private
-
- def unary_operations_using_variant_of_patience_algorithm
- operations = []
- aks, eks = actual.keys, expected.keys
- previous_ei, ei = nil, 0
- ai = 0
-
- # When diffing a hash, we're more interested in the 'actual' version
- # than the 'expected' version, because that's the ultimate truth.
- # Therefore, the diff is presented from the perspective of the 'actual'
- # hash, and we start off by looping over it.
- while ai < aks.size
- ak = aks[ai]
- av, ev = actual[ak], expected[ak]
- # While we iterate over 'actual' in order, we jump all over
- # 'expected', trying to match up its keys with the keys in 'actual' as
- # much as possible.
- ei = eks.index(ak)
-
- if should_add_noop_operation?(ak)
- # (If we're here, it probably means that the key we're pointing to
- # in the 'actual' and 'expected' hashes have the same value.)
-
- if ei && previous_ei && (ei - previous_ei) > 1
- # If we've jumped from one operation in the 'expected' hash to
- # another operation later in 'expected' (due to the fact that the
- # 'expected' hash is in a different order than 'actual'), collect
- # any delete operations in between and add them to our operations
- # array as deletes before adding the noop. If we don't do this
- # now, then those deletes will disappear. (Again, we are mainly
- # iterating over 'actual', so this is the only way to catch all of
- # the keys in 'expected'.)
- (previous_ei + 1).upto(ei - 1) do |ei2|
- ek = eks[ei2]
- ev2, av2 = expected[ek], actual[ek]
-
- if (
- (!actual.include?(ek) || ev2 != av2) &&
- operations.none? do |operation|
- %i[delete noop].include?(operation.name) &&
- operation.key == ek
- end
- )
- operations << Operations::UnaryOperation.new(
- name: :delete,
- collection: expected,
- key: ek,
- value: ev2,
- index: ei2
- )
- end
- end
- end
-
- operations << Operations::UnaryOperation.new(
- name: :noop,
- collection: actual,
- key: ak,
- value: av,
- index: ai
- )
- else
- # (If we're here, it probably means that the key in 'actual' isn't
- # present in 'expected' or the values don't match.)
-
- if (
- (operations.empty? || operations.last.name == :noop) &&
- (ai == 0 || eks.include?(aks[ai - 1]))
- )
- # If we go from a match in the last iteration to a missing or
- # extra key in this one, or we're at the first key in 'actual' and
- # it's missing or extra, look for deletes in the 'expected' hash
- # and add them to our list of operations before we add the
- # inserts. In most cases we will accomplish this by backtracking a
- # bit to the key in 'expected' that matched the key in 'actual' we
- # processed in the previous iteration (or just the first key in
- # 'expected' if this is the first key in 'actual'), and then
- # iterating from there through 'expected' until we reach the end
- # or we hit some other condition (see below).
-
- start_index =
- if ai > 0
- eks.index(aks[ai - 1]) + 1
- else
- 0
- end
-
- start_index.upto(eks.size - 1) do |ei2|
- ek = eks[ei2]
- ev, av2 = expected[ek], actual[ek]
-
- if actual.include?(ek) && ev == av2
- # If the key in 'expected' we've landed on happens to be a
- # match in 'actual', then stop, because it's going to be
- # handled in some future iteration of the 'actual' loop.
- break
- elsif (
- aks[ai + 1..-1].any? do |k|
- expected.include?(k) && expected[k] != actual[k]
- end
- )
- # While we backtracked a bit to iterate over 'expected', we
- # now have to look ahead. If we will end up encountering a
- # insert that matches this delete later, stop and go back to
- # iterating over 'actual'. This is because the delete we would
- # have added now will be added later when we encounter the
- # associated insert, so we don't want to add it twice.
- break
- else
- operations << Operations::UnaryOperation.new(
- name: :delete,
- collection: expected,
- key: ek,
- value: ev,
- index: ei2
- )
- end
-
- if ek == ak && ev != av
- # If we're pointing to the same key in 'expected' as in
- # 'actual', but with different values, go ahead and add an
- # insert now to accompany the delete added above. That way
- # they appear together, which will be easier to read.
- operations << Operations::UnaryOperation.new(
- name: :insert,
- collection: actual,
- key: ak,
- value: av,
- index: ai
- )
- end
- end
- end
-
- if (
- expected.include?(ak) && ev != av &&
- operations.none? { |op| op.name == :delete && op.key == ak }
- )
- # If we're here, it means that we didn't encounter any delete
- # operations above for whatever reason and so we need to add a
- # delete to represent the fact that the value for this key has
- # changed.
- operations << Operations::UnaryOperation.new(
- name: :delete,
- collection: expected,
- key: ak,
- value: expected[ak],
- index: ei
- )
- end
-
- if operations.none? { |op| op.name == :insert && op.key == ak }
- # If we're here, it means that we didn't encounter any insert
- # operations above. Since we already handled delete, the only
- # alternative is that this key must not exist in 'expected', so
- # we need to add an insert.
- operations << Operations::UnaryOperation.new(
- name: :insert,
- collection: actual,
- key: ak,
- value: av,
- index: ai
- )
- end
- end
-
- ai += 1
- previous_ei = ei
- end
-
- # The last thing to do is this: if there are keys in 'expected' that
- # aren't in 'actual', and they aren't associated with any inserts to
- # where they would have been added above, tack those deletes onto the
- # end of our operations array.
- (eks - aks - operations.map(&:key)).each do |ek|
- ei = eks.index(ek)
- ev = expected[ek]
-
- operations << Operations::UnaryOperation.new(
- name: :delete,
- collection: expected,
- key: ek,
- value: ev,
- index: ei
- )
- end
-
- operations
- end
-
- def should_add_noop_operation?(key)
- expected.include?(key) && expected[key] == actual[key]
- end
-
- def all_keys
- actual.keys | expected.keys
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_builders/multiline_string.rb b/lib/super_diff/operation_tree_builders/multiline_string.rb
deleted file mode 100644
index fdda3487..00000000
--- a/lib/super_diff/operation_tree_builders/multiline_string.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-module SuperDiff
- module OperationTreeBuilders
- class MultilineString < Base
- def self.applies_to?(expected, actual)
- expected.is_a?(::String) && actual.is_a?(::String) &&
- (expected.include?("\n") || actual.include?("\n"))
- end
-
- def initialize(*args)
- super(*args)
-
- @original_expected = @expected
- @original_actual = @actual
- @expected = split_into_lines(@expected)
- @actual = split_into_lines(@actual)
- @sequence_matcher = PatienceDiff::SequenceMatcher.new
- end
-
- protected
-
- def unary_operations
- opcodes.flat_map do |code, a_start, a_end, b_start, b_end|
- if code == :delete
- add_delete_operations(a_start..a_end)
- elsif code == :insert
- add_insert_operations(b_start..b_end)
- else
- add_noop_operations(b_start..b_end)
- end
- end
- end
-
- def build_operation_tree
- OperationTrees::MultilineString.new([])
- end
-
- private
-
- attr_reader :sequence_matcher, :original_expected, :original_actual
-
- def split_into_lines(string)
- string.scan(/.+(?:\r|\n|\r\n|\Z)/)
- end
-
- def opcodes
- sequence_matcher.diff_opcodes(expected, actual)
- end
-
- def add_delete_operations(indices)
- indices.map do |index|
- Operations::UnaryOperation.new(
- name: :delete,
- collection: expected,
- key: index,
- index: index,
- value: expected[index]
- )
- end
- end
-
- def add_insert_operations(indices)
- indices.map do |index|
- Operations::UnaryOperation.new(
- name: :insert,
- collection: actual,
- key: index,
- index: index,
- value: actual[index]
- )
- end
- end
-
- def add_noop_operations(indices)
- indices.map do |index|
- Operations::UnaryOperation.new(
- name: :noop,
- collection: actual,
- key: index,
- index: index,
- value: actual[index]
- )
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_builders/time_like.rb b/lib/super_diff/operation_tree_builders/time_like.rb
deleted file mode 100644
index 18151f47..00000000
--- a/lib/super_diff/operation_tree_builders/time_like.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module SuperDiff
- module OperationTreeBuilders
- class TimeLike < CustomObject
- def self.applies_to?(expected, actual)
- SuperDiff.time_like?(expected) && SuperDiff.time_like?(actual)
- end
-
- protected
-
- def attribute_names
- base = %w[year month day hour min sec subsec zone utc_offset]
-
- # If timezones are different, also show a normalized timestamp at the
- # end of the diff to help visualize why they are different moments in
- # time.
- if actual.zone != expected.zone
- base + ["utc"]
- else
- base
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_flatteners.rb b/lib/super_diff/operation_tree_flatteners.rb
index 5e1df2a9..f732f0d9 100644
--- a/lib/super_diff/operation_tree_flatteners.rb
+++ b/lib/super_diff/operation_tree_flatteners.rb
@@ -1,20 +1,23 @@
module SuperDiff
module OperationTreeFlatteners
- autoload :Array, "super_diff/operation_tree_flatteners/array"
- autoload :Base, "super_diff/operation_tree_flatteners/base"
- autoload :Collection, "super_diff/operation_tree_flatteners/collection"
- autoload(
- :CustomObject,
- "super_diff/operation_tree_flatteners/custom_object"
- )
- autoload(
- :DefaultObject,
- "super_diff/operation_tree_flatteners/default_object"
- )
- autoload :Hash, "super_diff/operation_tree_flatteners/hash"
- autoload(
- :MultilineString,
- "super_diff/operation_tree_flatteners/multiline_string"
- )
+ def self.const_missing(missing_const_name)
+ if missing_const_name == :Base
+ warn <<~EOT
+ WARNING: SuperDiff::OperationTreeFlatteners::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::AbstractOperationTreeFlattener instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core::AbstractOperationTreeFlattener
+ elsif Basic::OperationTreeFlatteners.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::OperationTreeFlatteners::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Basic::OperationTreeFlatteners::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Basic::OperationTreeFlatteners.const_get(missing_const_name)
+ else
+ super
+ end
+ end
end
end
diff --git a/lib/super_diff/operation_tree_flatteners/array.rb b/lib/super_diff/operation_tree_flatteners/array.rb
deleted file mode 100644
index 56b8dc40..00000000
--- a/lib/super_diff/operation_tree_flatteners/array.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module OperationTreeFlatteners
- class Array < Collection
- protected
-
- def open_token
- "["
- end
-
- def close_token
- "]"
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_flatteners/collection.rb b/lib/super_diff/operation_tree_flatteners/collection.rb
deleted file mode 100644
index 0fc65117..00000000
--- a/lib/super_diff/operation_tree_flatteners/collection.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-module SuperDiff
- module OperationTreeFlatteners
- class Collection < Base
- protected
-
- def build_tiered_lines
- [
- Line.new(
- type: :noop,
- indentation_level: indentation_level,
- value: open_token,
- collection_bookend: :open
- ),
- *inner_lines,
- Line.new(
- type: :noop,
- indentation_level: indentation_level,
- value: close_token,
- collection_bookend: :close
- )
- ]
- end
-
- def inner_lines
- @_inner_lines ||=
- operation_tree.flat_map do |operation|
- lines =
- if operation.name == :change
- build_lines_for_change_operation(operation)
- else
- build_lines_for_non_change_operation(operation)
- end
-
- maybe_add_prefix_at_beginning_of_lines(
- maybe_add_comma_at_end_of_lines(lines, operation),
- operation
- )
- end
- end
-
- def maybe_add_prefix_at_beginning_of_lines(lines, operation)
- if add_prefix_at_beginning_of_lines?(operation)
- add_prefix_at_beginning_of_lines(lines, operation)
- else
- lines
- end
- end
-
- def add_prefix_at_beginning_of_lines?(operation)
- !!item_prefix_for(operation)
- end
-
- def add_prefix_at_beginning_of_lines(lines, operation)
- [lines[0].prefixed_with(item_prefix_for(operation))] + lines[1..-1]
- end
-
- def maybe_add_comma_at_end_of_lines(lines, operation)
- if last_item_in_collection?(operation)
- lines
- else
- add_comma_at_end_of_lines(lines)
- end
- end
-
- def last_item_in_collection?(operation)
- if operation.name == :change
- operation.left_index == operation.left_collection.size - 1 &&
- operation.right_index == operation.right_collection.size - 1
- else
- operation.index == operation.collection.size - 1
- end
- end
-
- def add_comma_at_end_of_lines(lines)
- lines[0..-2] + [lines[-1].with_comma]
- end
-
- def build_lines_for_change_operation(operation)
- SuperDiff::RecursionGuard.guarding_recursion_of(
- operation.left_collection,
- operation.right_collection
- ) do |already_seen|
- if already_seen
- raise InfiniteRecursionError
- else
- operation.children.flatten(indentation_level: indentation_level + 1)
- end
- end
- end
-
- def build_lines_for_non_change_operation(operation)
- indentation_level = @indentation_level + 1
-
- if recursive_operation?(operation)
- [
- Line.new(
- type: operation.name,
- indentation_level: indentation_level,
- value: SuperDiff::RecursionGuard::PLACEHOLDER
- )
- ]
- else
- build_lines_from_inspection_of(
- operation.value,
- type: operation.name,
- indentation_level: indentation_level
- )
- end
- end
-
- def recursive_operation?(operation)
- operation.value.equal?(operation.collection) ||
- SuperDiff::RecursionGuard.already_seen?(operation.value)
- end
-
- def item_prefix_for(_operation)
- ""
- end
-
- def build_lines_from_inspection_of(value, type:, indentation_level:)
- SuperDiff.inspect_object(
- value,
- as_lines: true,
- type: type,
- indentation_level: indentation_level
- )
- end
-
- class InfiniteRecursionError < StandardError
- def initialize(_message = nil)
- super("Unhandled recursive data structure encountered!")
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_flatteners/custom_object.rb b/lib/super_diff/operation_tree_flatteners/custom_object.rb
deleted file mode 100644
index 38e08bdd..00000000
--- a/lib/super_diff/operation_tree_flatteners/custom_object.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module SuperDiff
- module OperationTreeFlatteners
- class CustomObject < Collection
- protected
-
- def open_token
- "#<%s {" % { class: operation_tree.underlying_object.class }
- end
-
- def close_token
- "}>"
- end
-
- def item_prefix_for(operation)
- key =
- # Note: We could have used the right_key here too, they're both the
- # same keys
- if operation.respond_to?(:left_key)
- operation.left_key
- else
- operation.key
- end
-
- "#{key}: "
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_flatteners/default_object.rb b/lib/super_diff/operation_tree_flatteners/default_object.rb
deleted file mode 100644
index 8966557e..00000000
--- a/lib/super_diff/operation_tree_flatteners/default_object.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module SuperDiff
- module OperationTreeFlatteners
- class DefaultObject < Collection
- protected
-
- def open_token
- "#<#{operation_tree.underlying_object.class.name}:" +
- SuperDiff::Helpers.object_address_for(
- operation_tree.underlying_object
- ) + " {"
- end
-
- def close_token
- "}>"
- end
-
- def item_prefix_for(operation)
- key =
- # Note: We could have used the right_key here too, they're both the
- # same keys
- if operation.respond_to?(:left_key)
- operation.left_key
- else
- operation.key
- end
-
- "@#{key}="
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_flatteners/hash.rb b/lib/super_diff/operation_tree_flatteners/hash.rb
deleted file mode 100644
index 853d1f08..00000000
--- a/lib/super_diff/operation_tree_flatteners/hash.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module SuperDiff
- module OperationTreeFlatteners
- class Hash < Collection
- protected
-
- def open_token
- "{"
- end
-
- def close_token
- "}"
- end
-
- def item_prefix_for(operation)
- key = key_for(operation)
-
- format_keys_as_kwargs? ? "#{key}: " : "#{key.inspect} => "
- end
-
- private
-
- def format_keys_as_kwargs?
- operation_tree.all? { |operation| key_for(operation).is_a?(Symbol) }
- end
-
- def key_for(operation)
- # Note: We could have used the right_key here too, they're both the
- # same keys
- operation.respond_to?(:left_key) ? operation.left_key : operation.key
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_tree_flatteners/multiline_string.rb b/lib/super_diff/operation_tree_flatteners/multiline_string.rb
deleted file mode 100644
index 9f4fa83d..00000000
--- a/lib/super_diff/operation_tree_flatteners/multiline_string.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module SuperDiff
- module OperationTreeFlatteners
- class MultilineString < Base
- def build_tiered_lines
- operation_tree.map do |operation|
- Line.new(
- type: operation.name,
- indentation_level: indentation_level,
- # TODO: Test that quotes and things don't get escaped but escape
- # characters do
- value:
- operation.value.inspect[1..-2].gsub(/\\"/, '"').gsub(/\\'/, "'")
- )
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_trees.rb b/lib/super_diff/operation_trees.rb
index 2075ef77..3dc3e257 100644
--- a/lib/super_diff/operation_trees.rb
+++ b/lib/super_diff/operation_trees.rb
@@ -1,13 +1,23 @@
module SuperDiff
module OperationTrees
- autoload :Array, "super_diff/operation_trees/array"
- autoload :Base, "super_diff/operation_trees/base"
- autoload :CustomObject, "super_diff/operation_trees/custom_object"
- autoload :DefaultObject, "super_diff/operation_trees/default_object"
- autoload :Hash, "super_diff/operation_trees/hash"
- autoload :Main, "super_diff/operation_trees/main"
- autoload :MultilineString, "super_diff/operation_trees/multiline_string"
+ def self.const_missing(missing_const_name)
+ if missing_const_name == :Base
+ warn <<~EOT
+ WARNING: SuperDiff::OperationTrees::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::AbstractOperationTree instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core::AbstractOperationTree
+ elsif Basic::OperationTrees.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::OperationTrees::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Basic::OperationTrees::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Basic::OperationTrees.const_get(missing_const_name)
+ else
+ super
+ end
+ end
end
end
-
-require "super_diff/operation_trees/defaults"
diff --git a/lib/super_diff/operation_trees/array.rb b/lib/super_diff/operation_trees/array.rb
deleted file mode 100644
index 63458e12..00000000
--- a/lib/super_diff/operation_trees/array.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module OperationTrees
- class Array < Base
- def self.applies_to?(value)
- value.is_a?(::Array)
- end
-
- protected
-
- def operation_tree_flattener_class
- OperationTreeFlatteners::Array
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_trees/custom_object.rb b/lib/super_diff/operation_trees/custom_object.rb
deleted file mode 100644
index 7e75467e..00000000
--- a/lib/super_diff/operation_trees/custom_object.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module OperationTrees
- class CustomObject < DefaultObject
- def self.applies_to?(value)
- value.respond_to?(:attributes_for_super_diff)
- end
-
- protected
-
- def operation_tree_flattener_class
- OperationTreeFlatteners::CustomObject
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_trees/default_object.rb b/lib/super_diff/operation_trees/default_object.rb
deleted file mode 100644
index 4ccfb6b0..00000000
--- a/lib/super_diff/operation_trees/default_object.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-module SuperDiff
- module OperationTrees
- class DefaultObject < Base
- def self.applies_to?(*)
- true
- end
-
- attr_reader :underlying_object
-
- def initialize(operations, underlying_object:)
- super(operations)
- @underlying_object = underlying_object
- end
-
- def pretty_print(pp)
- pp.text "#<#{self.class.name} "
- pp.nest(1) do
- pp.breakable
- pp.text ":operations=>"
- pp.group(1, "[", "]") do
- pp.breakable
- pp.seplist(self) { |value| pp.pp value }
- end
- pp.comma_breakable
- pp.text ":underlying_object=>"
- pp.object_address_group underlying_object do
- # do nothing
- end
- end
- pp.text ">"
- end
-
- protected
-
- def operation_tree_flattener_class
- OperationTreeFlatteners::DefaultObject
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_trees/defaults.rb b/lib/super_diff/operation_trees/defaults.rb
deleted file mode 100644
index dd3dad60..00000000
--- a/lib/super_diff/operation_trees/defaults.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module SuperDiff
- module OperationTrees
- DEFAULTS = [Array, Hash, CustomObject, DefaultObject].freeze
- end
-end
diff --git a/lib/super_diff/operation_trees/hash.rb b/lib/super_diff/operation_trees/hash.rb
deleted file mode 100644
index 9f3f64ff..00000000
--- a/lib/super_diff/operation_trees/hash.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module OperationTrees
- class Hash < Base
- def self.applies_to?(value)
- value.is_a?(::Hash)
- end
-
- protected
-
- def operation_tree_flattener_class
- OperationTreeFlatteners::Hash
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_trees/main.rb b/lib/super_diff/operation_trees/main.rb
deleted file mode 100644
index 584d5ccb..00000000
--- a/lib/super_diff/operation_trees/main.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module SuperDiff
- module OperationTrees
- class Main
- extend AttrExtras.mixin
-
- method_object :value
-
- def call
- if resolved_class
- begin
- resolved_class.new([], underlying_object: value)
- rescue ArgumentError
- resolved_class.new([])
- end
- else
- raise Errors::NoOperationalSequenceAvailableError.create(value)
- end
- end
-
- private
-
- def resolved_class
- if value.respond_to?(:attributes_for_super_diff)
- CustomObject
- else
- available_classes.find { |klass| klass.applies_to?(value) }
- end
- end
-
- def available_classes
- SuperDiff.configuration.extra_operation_tree_classes + DEFAULTS
- end
- end
- end
-end
diff --git a/lib/super_diff/operation_trees/multiline_string.rb b/lib/super_diff/operation_trees/multiline_string.rb
deleted file mode 100644
index 95d3e6c4..00000000
--- a/lib/super_diff/operation_trees/multiline_string.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module SuperDiff
- module OperationTrees
- class MultilineString < Base
- def self.applies_to?(value)
- value.is_a?(::String) && value.is_a?(::String)
- end
-
- protected
-
- def operation_tree_flattener_class
- OperationTreeFlatteners::MultilineString
- end
- end
- end
-end
diff --git a/lib/super_diff/operations.rb b/lib/super_diff/operations.rb
index 9b587b35..dc40c224 100644
--- a/lib/super_diff/operations.rb
+++ b/lib/super_diff/operations.rb
@@ -1,6 +1,16 @@
module SuperDiff
module Operations
- autoload :BinaryOperation, "super_diff/operations/binary_operation"
- autoload :UnaryOperation, "super_diff/operations/unary_operation"
+ def self.const_missing(missing_const_name)
+ if Core.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::Operations::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::Core::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ Core.const_get(missing_const_name)
+ else
+ super
+ end
+ end
end
end
diff --git a/lib/super_diff/recursion_guard.rb b/lib/super_diff/recursion_guard.rb
deleted file mode 100644
index 1e8763eb..00000000
--- a/lib/super_diff/recursion_guard.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require "set"
-
-module SuperDiff
- module RecursionGuard
- RECURSION_GUARD_KEY = "super_diff_recursion_guard_key".freeze
- PLACEHOLDER = "âââ".freeze
-
- def self.guarding_recursion_of(*objects, &block)
- already_seen_objects, first_seen_objects =
- objects.partition do |object|
- !SuperDiff.primitive?(object) && already_seen?(object)
- end
-
- first_seen_objects.each do |object|
- already_seen_object_ids.add(object.object_id)
- end
-
- result =
- if block.arity > 0
- block.call(already_seen_objects.any?)
- else
- block.call
- end
-
- first_seen_objects.each do |object|
- already_seen_object_ids.delete(object.object_id)
- end
-
- result
- end
-
- def self.substituting_recursion_of(*objects)
- guarding_recursion_of(*objects) do |already_seen|
- if already_seen
- PLACEHOLDER
- else
- yield
- end
- end
- end
-
- def self.already_seen?(object)
- already_seen_object_ids.include?(object.object_id)
- end
-
- def self.already_seen_object_ids
- Thread.current[RECURSION_GUARD_KEY] ||= Set.new
- end
- end
-end
diff --git a/lib/super_diff/rspec.rb b/lib/super_diff/rspec.rb
index e9a09692..70bc5325 100644
--- a/lib/super_diff/rspec.rb
+++ b/lib/super_diff/rspec.rb
@@ -1,15 +1,17 @@
require "super_diff"
+require "super_diff/rspec/differs"
+require "super_diff/rspec/inspection_tree_builders"
+require "super_diff/rspec/operation_tree_builders"
+
module SuperDiff
module RSpec
autoload :AugmentedMatcher, "super_diff/rspec/augmented_matcher"
autoload :Configuration, "super_diff/rspec/configuration"
autoload :Differ, "super_diff/rspec/differ"
- autoload :Differs, "super_diff/rspec/differs"
autoload :MatcherTextBuilders, "super_diff/rspec/matcher_text_builders"
autoload :MatcherTextTemplate, "super_diff/rspec/matcher_text_template"
autoload :ObjectInspection, "super_diff/rspec/object_inspection"
- autoload :OperationTreeBuilders, "super_diff/rspec/operation_tree_builders"
def self.configure(&block)
SuperDiff.configure(&block)
@@ -84,37 +86,37 @@ def self.rspec_version
@_rspec_version ||=
begin
require "rspec/core/version"
- GemVersion.new(::RSpec::Core::Version::STRING)
+ Core::GemVersion.new(::RSpec::Core::Version::STRING)
end
end
SuperDiff.configuration.tap do |config|
- config.add_extra_differ_classes(
+ config.prepend_extra_differ_classes(
Differs::CollectionContainingExactly,
Differs::CollectionIncluding,
Differs::HashIncluding,
Differs::ObjectHavingAttributes
)
- config.add_extra_operation_tree_builder_classes(
+ config.prepend_extra_inspection_tree_builder_classes(
+ InspectionTreeBuilders::Double,
+ InspectionTreeBuilders::CollectionContainingExactly,
+ InspectionTreeBuilders::CollectionIncluding,
+ InspectionTreeBuilders::HashIncluding,
+ InspectionTreeBuilders::InstanceOf,
+ InspectionTreeBuilders::KindOf,
+ InspectionTreeBuilders::ObjectHavingAttributes,
+ # ObjectInspection::InspectionTreeBuilders::Primitive,
+ InspectionTreeBuilders::ValueWithin,
+ InspectionTreeBuilders::GenericDescribableMatcher
+ )
+
+ config.prepend_extra_operation_tree_builder_classes(
OperationTreeBuilders::CollectionContainingExactly,
OperationTreeBuilders::CollectionIncluding,
OperationTreeBuilders::HashIncluding,
OperationTreeBuilders::ObjectHavingAttributes
)
-
- config.add_extra_inspection_tree_builder_classes(
- ObjectInspection::InspectionTreeBuilders::Double,
- ObjectInspection::InspectionTreeBuilders::CollectionContainingExactly,
- ObjectInspection::InspectionTreeBuilders::CollectionIncluding,
- ObjectInspection::InspectionTreeBuilders::HashIncluding,
- ObjectInspection::InspectionTreeBuilders::InstanceOf,
- ObjectInspection::InspectionTreeBuilders::KindOf,
- ObjectInspection::InspectionTreeBuilders::ObjectHavingAttributes,
- # ObjectInspection::InspectionTreeBuilders::Primitive,
- ObjectInspection::InspectionTreeBuilders::ValueWithin,
- ObjectInspection::InspectionTreeBuilders::GenericDescribableMatcher
- )
end
end
end
diff --git a/lib/super_diff/rspec/augmented_matcher.rb b/lib/super_diff/rspec/augmented_matcher.rb
index 80a30468..54dd8521 100644
--- a/lib/super_diff/rspec/augmented_matcher.rb
+++ b/lib/super_diff/rspec/augmented_matcher.rb
@@ -35,7 +35,7 @@ def matcher_text_builder
end
def matcher_text_builder_class
- RSpec::MatcherTextBuilders::Base
+ MatcherTextBuilders::Base
end
def matcher_text_builder_args
diff --git a/lib/super_diff/rspec/differ.rb b/lib/super_diff/rspec/differ.rb
index 2f41b0f5..538dccd1 100644
--- a/lib/super_diff/rspec/differ.rb
+++ b/lib/super_diff/rspec/differ.rb
@@ -7,13 +7,12 @@ class Differ
def diff
if worth_diffing?
- diff =
- SuperDiff::Differs::Main.call(expected, actual, omit_empty: true)
+ diff = SuperDiff.diff(expected, actual)
"\n\n" + diff
else
""
end
- rescue SuperDiff::Errors::NoDifferAvailableError
+ rescue Core::NoDifferAvailableError
""
end
@@ -39,10 +38,10 @@ def comparing_singleline_strings?
end
def helpers
- @_helpers ||= Helpers.new
+ @_helpers ||= RSpecHelpers.new
end
- class Helpers
+ class RSpecHelpers
include ::RSpec::Matchers::Composable
public :values_match?
diff --git a/lib/super_diff/rspec/differs/collection_containing_exactly.rb b/lib/super_diff/rspec/differs/collection_containing_exactly.rb
index 8061c66f..89ba4e6d 100644
--- a/lib/super_diff/rspec/differs/collection_containing_exactly.rb
+++ b/lib/super_diff/rspec/differs/collection_containing_exactly.rb
@@ -1,7 +1,7 @@
module SuperDiff
module RSpec
module Differs
- class CollectionContainingExactly < SuperDiff::Differs::Array
+ class CollectionContainingExactly < Basic::Differs::Array
def self.applies_to?(expected, actual)
SuperDiff::RSpec.a_collection_containing_exactly_something?(
expected
diff --git a/lib/super_diff/rspec/differs/collection_including.rb b/lib/super_diff/rspec/differs/collection_including.rb
index d1cd7c2e..83e08994 100644
--- a/lib/super_diff/rspec/differs/collection_including.rb
+++ b/lib/super_diff/rspec/differs/collection_including.rb
@@ -1,7 +1,7 @@
module SuperDiff
module RSpec
module Differs
- class CollectionIncluding < SuperDiff::Differs::Array
+ class CollectionIncluding < Basic::Differs::Array
def self.applies_to?(expected, actual)
(
SuperDiff::RSpec.a_collection_including_something?(expected) ||
diff --git a/lib/super_diff/rspec/differs/hash_including.rb b/lib/super_diff/rspec/differs/hash_including.rb
index 5df1cb9c..91255d5d 100644
--- a/lib/super_diff/rspec/differs/hash_including.rb
+++ b/lib/super_diff/rspec/differs/hash_including.rb
@@ -1,7 +1,7 @@
module SuperDiff
module RSpec
module Differs
- class HashIncluding < SuperDiff::Differs::Hash
+ class HashIncluding < Basic::Differs::Hash
def self.applies_to?(expected, actual)
(
SuperDiff::RSpec.a_hash_including_something?(expected) ||
diff --git a/lib/super_diff/rspec/differs/object_having_attributes.rb b/lib/super_diff/rspec/differs/object_having_attributes.rb
index e096b3dd..d40309f9 100644
--- a/lib/super_diff/rspec/differs/object_having_attributes.rb
+++ b/lib/super_diff/rspec/differs/object_having_attributes.rb
@@ -1,7 +1,7 @@
module SuperDiff
module RSpec
module Differs
- class ObjectHavingAttributes < SuperDiff::Differs::DefaultObject
+ class ObjectHavingAttributes < Basic::Differs::DefaultObject
def self.applies_to?(expected, _actual)
SuperDiff::RSpec.an_object_having_some_attributes?(expected)
end
diff --git a/lib/super_diff/rspec/inspection_tree_builders.rb b/lib/super_diff/rspec/inspection_tree_builders.rb
new file mode 100644
index 00000000..52936c35
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders.rb
@@ -0,0 +1,40 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ autoload(
+ :CollectionContainingExactly,
+ "super_diff/rspec/inspection_tree_builders/collection_containing_exactly"
+ )
+ autoload(
+ :CollectionIncluding,
+ "super_diff/rspec/inspection_tree_builders/collection_including"
+ )
+ autoload :Double, "super_diff/rspec/inspection_tree_builders/double"
+ autoload(
+ :GenericDescribableMatcher,
+ "super_diff/rspec/inspection_tree_builders/generic_describable_matcher"
+ )
+ autoload(
+ :HashIncluding,
+ "super_diff/rspec/inspection_tree_builders/hash_including"
+ )
+ autoload(
+ :InstanceOf,
+ "super_diff/rspec/inspection_tree_builders/instance_of"
+ )
+ autoload :KindOf, "super_diff/rspec/inspection_tree_builders/kind_of"
+ autoload(
+ :ObjectHavingAttributes,
+ "super_diff/rspec/inspection_tree_builders/object_having_attributes"
+ )
+ autoload(
+ :Primitive,
+ "super_diff/rspec/inspection_tree_builders/primitive"
+ )
+ autoload(
+ :ValueWithin,
+ "super_diff/rspec/inspection_tree_builders/value_within"
+ )
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/collection_containing_exactly.rb b/lib/super_diff/rspec/inspection_tree_builders/collection_containing_exactly.rb
new file mode 100644
index 00000000..db167561
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/collection_containing_exactly.rb
@@ -0,0 +1,34 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ class CollectionContainingExactly < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ SuperDiff::RSpec.a_collection_containing_exactly_something?(value)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ # stree-ignore
+ t1.as_lines_when_rendering_to_lines(
+ collection_bookend: :open
+ ) do |t2|
+ t2.add_text "#"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/collection_including.rb b/lib/super_diff/rspec/inspection_tree_builders/collection_including.rb
new file mode 100644
index 00000000..ff426a16
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/collection_including.rb
@@ -0,0 +1,40 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ class CollectionIncluding < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ SuperDiff::RSpec.a_collection_including_something?(value) ||
+ SuperDiff::RSpec.array_including_something?(value)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ # stree-ignore
+ t1.as_lines_when_rendering_to_lines(
+ collection_bookend: :open
+ ) do |t2|
+ t2.add_text "#"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/double.rb b/lib/super_diff/rspec/inspection_tree_builders/double.rb
new file mode 100644
index 00000000..4eb42775
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/double.rb
@@ -0,0 +1,100 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ class Double < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ value.is_a?(::RSpec::Mocks::Double)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ t1.only_when method(:empty?) do |t2|
+ t2.as_lines_when_rendering_to_lines do |t3|
+ t3.add_text("#<#{inspected_class} #{inspected_name}>")
+ end
+ end
+
+ t1.only_when method(:nonempty?) do |t2|
+ t2.as_lines_when_rendering_to_lines(
+ collection_bookend: :open
+ ) do |t3|
+ t3.add_text("#<#{inspected_class} #{inspected_name}")
+
+ # stree-ignore
+ t3.when_rendering_to_lines do |t4|
+ t4.add_text " {"
+ end
+ end
+
+ # stree-ignore
+ t2.when_rendering_to_string do |t3|
+ t3.add_text " "
+ end
+
+ # stree-ignore
+ t2.nested do |t3|
+ t3.insert_hash_inspection_of doubled_methods
+ end
+
+ t2.as_lines_when_rendering_to_lines(
+ collection_bookend: :close
+ ) do |t3|
+ # stree-ignore
+ t3.when_rendering_to_lines do |t4|
+ t4.add_text "}"
+ end
+
+ t3.add_text ">"
+ end
+ end
+ end
+ end
+
+ private
+
+ def empty?
+ doubled_methods.empty?
+ end
+
+ def nonempty?
+ !empty?
+ end
+
+ def inspected_class
+ case object
+ when ::RSpec::Mocks::InstanceVerifyingDouble
+ "InstanceDouble"
+ when ::RSpec::Mocks::ClassVerifyingDouble
+ "ClassDouble"
+ when ::RSpec::Mocks::ObjectVerifyingDouble
+ "ObjectDouble"
+ else
+ "Double"
+ end
+ end
+
+ def inspected_name
+ if object.instance_variable_get("@name")
+ object.instance_variable_get("@name").inspect
+ else
+ "(anonymous)"
+ end
+ end
+
+ def doubled_methods
+ @_doubled_methods ||=
+ doubled_method_names.reduce({}) do |hash, key|
+ hash.merge(key => object.public_send(key))
+ end
+ end
+
+ def doubled_method_names
+ object
+ .__send__(:__mock_proxy)
+ .instance_variable_get("@method_doubles")
+ .keys
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/generic_describable_matcher.rb b/lib/super_diff/rspec/inspection_tree_builders/generic_describable_matcher.rb
new file mode 100644
index 00000000..309fc01f
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/generic_describable_matcher.rb
@@ -0,0 +1,17 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ class GenericDescribableMatcher < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ ::RSpec::Matchers.is_a_describable_matcher?(value)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ t1.add_text "#<#{object.description}>"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/hash_including.rb b/lib/super_diff/rspec/inspection_tree_builders/hash_including.rb
new file mode 100644
index 00000000..a9936d07
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/hash_including.rb
@@ -0,0 +1,40 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ class HashIncluding < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ SuperDiff::RSpec.a_hash_including_something?(value) ||
+ SuperDiff::RSpec.hash_including_something?(value)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ # stree-ignore
+ t1.as_lines_when_rendering_to_lines(
+ collection_bookend: :open
+ ) do |t2|
+ t2.add_text "#"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/instance_of.rb b/lib/super_diff/rspec/inspection_tree_builders/instance_of.rb
new file mode 100644
index 00000000..55802150
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/instance_of.rb
@@ -0,0 +1,25 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ class InstanceOf < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ SuperDiff::RSpec.an_instance_of_something?(value) ||
+ SuperDiff::RSpec.instance_of_something?(value)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ klass =
+ if SuperDiff::RSpec.an_instance_of_something?(object)
+ object.expected
+ else
+ object.instance_variable_get(:@klass)
+ end
+
+ t1.add_text "#"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/kind_of.rb b/lib/super_diff/rspec/inspection_tree_builders/kind_of.rb
new file mode 100644
index 00000000..c79bd63a
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/kind_of.rb
@@ -0,0 +1,25 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ class KindOf < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ SuperDiff::RSpec.a_kind_of_something?(value) ||
+ SuperDiff::RSpec.kind_of_something?(value)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ klass =
+ if SuperDiff::RSpec.a_kind_of_something?(object)
+ object.expected
+ else
+ object.instance_variable_get(:@klass)
+ end
+
+ t1.add_text "#"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/object_having_attributes.rb b/lib/super_diff/rspec/inspection_tree_builders/object_having_attributes.rb
new file mode 100644
index 00000000..bfddd486
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/object_having_attributes.rb
@@ -0,0 +1,34 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ class ObjectHavingAttributes < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ SuperDiff::RSpec.an_object_having_some_attributes?(value)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ # stree-ignore
+ t1.as_lines_when_rendering_to_lines(
+ collection_bookend: :open
+ ) do |t2|
+ t2.add_text "#"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/primitive.rb b/lib/super_diff/rspec/inspection_tree_builders/primitive.rb
new file mode 100644
index 00000000..929894d9
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/primitive.rb
@@ -0,0 +1,9 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ # TODO: Is this needed?
+ class Primitive < Basic::InspectionTreeBuilders::Primitive
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/inspection_tree_builders/value_within.rb b/lib/super_diff/rspec/inspection_tree_builders/value_within.rb
new file mode 100644
index 00000000..0b611b93
--- /dev/null
+++ b/lib/super_diff/rspec/inspection_tree_builders/value_within.rb
@@ -0,0 +1,30 @@
+module SuperDiff
+ module RSpec
+ module InspectionTreeBuilders
+ class ValueWithin < Core::AbstractInspectionTreeBuilder
+ def self.applies_to?(value)
+ SuperDiff::RSpec.a_value_within_something?(value)
+ end
+
+ def call
+ Core::InspectionTree.new do |t1|
+ t1.as_prelude_when_rendering_to_lines do |t2|
+ t2.add_text "#"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/super_diff/rspec/object_inspection.rb b/lib/super_diff/rspec/object_inspection.rb
index 9de683af..d45e21cd 100644
--- a/lib/super_diff/rspec/object_inspection.rb
+++ b/lib/super_diff/rspec/object_inspection.rb
@@ -1,10 +1,20 @@
module SuperDiff
module RSpec
module ObjectInspection
- autoload(
- :InspectionTreeBuilders,
- "super_diff/rspec/object_inspection/inspection_tree_builders"
- )
+ module InspectionTreeBuilders
+ def self.const_missing(missing_const_name)
+ if RSpec::InspectionTreeBuilders.const_defined?(missing_const_name)
+ warn <<~EOT
+ WARNING: SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::#{missing_const_name} is deprecated and will be removed in the next major release.
+ Please use SuperDiff::RSpec::InspectionTreeBuilders::#{missing_const_name} instead.
+ #{caller_locations.join("\n")}
+ EOT
+ RSpec::InspectionTreeBuilders.const_get(missing_const_name)
+ else
+ super
+ end
+ end
+ end
end
end
end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders.rb
deleted file mode 100644
index d14650cf..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- autoload(
- :CollectionContainingExactly,
- "super_diff/rspec/object_inspection/inspection_tree_builders/collection_containing_exactly"
- )
- autoload(
- :CollectionIncluding,
- "super_diff/rspec/object_inspection/inspection_tree_builders/collection_including"
- )
- autoload(
- :Double,
- "super_diff/rspec/object_inspection/inspection_tree_builders/double"
- )
- autoload(
- :GenericDescribableMatcher,
- "super_diff/rspec/object_inspection/inspection_tree_builders/generic_describable_matcher"
- )
- autoload(
- :HashIncluding,
- "super_diff/rspec/object_inspection/inspection_tree_builders/hash_including"
- )
- autoload(
- :InstanceOf,
- "super_diff/rspec/object_inspection/inspection_tree_builders/instance_of"
- )
- autoload(
- :KindOf,
- "super_diff/rspec/object_inspection/inspection_tree_builders/kind_of"
- )
- autoload(
- :ObjectHavingAttributes,
- "super_diff/rspec/object_inspection/inspection_tree_builders/object_having_attributes"
- )
- autoload(
- :Primitive,
- "super_diff/rspec/object_inspection/inspection_tree_builders/primitive"
- )
- autoload(
- :ValueWithin,
- "super_diff/rspec/object_inspection/inspection_tree_builders/value_within"
- )
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/collection_containing_exactly.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/collection_containing_exactly.rb
deleted file mode 100644
index b64325f1..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/collection_containing_exactly.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class CollectionContainingExactly < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- SuperDiff::RSpec.a_collection_containing_exactly_something?(value)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- # stree-ignore
- t1.as_lines_when_rendering_to_lines(
- collection_bookend: :open
- ) do |t2|
- t2.add_text "#"
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/collection_including.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/collection_including.rb
deleted file mode 100644
index cb4b6887..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/collection_including.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class CollectionIncluding < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- SuperDiff::RSpec.a_collection_including_something?(value) ||
- SuperDiff::RSpec.array_including_something?(value)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- # stree-ignore
- t1.as_lines_when_rendering_to_lines(
- collection_bookend: :open
- ) do |t2|
- t2.add_text "#"
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/double.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/double.rb
deleted file mode 100644
index 8278db45..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/double.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class Double < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- value.is_a?(::RSpec::Mocks::Double)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- t1.only_when method(:empty?) do |t2|
- t2.as_lines_when_rendering_to_lines do |t3|
- t3.add_text("#<#{inspected_class} #{inspected_name}>")
- end
- end
-
- t1.only_when method(:nonempty?) do |t2|
- t2.as_lines_when_rendering_to_lines(
- collection_bookend: :open
- ) do |t3|
- t3.add_text("#<#{inspected_class} #{inspected_name}")
-
- # stree-ignore
- t3.when_rendering_to_lines do |t4|
- t4.add_text " {"
- end
- end
-
- # stree-ignore
- t2.when_rendering_to_string do |t3|
- t3.add_text " "
- end
-
- # stree-ignore
- t2.nested do |t3|
- t3.insert_hash_inspection_of doubled_methods
- end
-
- t2.as_lines_when_rendering_to_lines(
- collection_bookend: :close
- ) do |t3|
- # stree-ignore
- t3.when_rendering_to_lines do |t4|
- t4.add_text "}"
- end
-
- t3.add_text ">"
- end
- end
- end
- end
-
- private
-
- def empty?
- doubled_methods.empty?
- end
-
- def nonempty?
- !empty?
- end
-
- def inspected_class
- case object
- when ::RSpec::Mocks::InstanceVerifyingDouble
- "InstanceDouble"
- when ::RSpec::Mocks::ClassVerifyingDouble
- "ClassDouble"
- when ::RSpec::Mocks::ObjectVerifyingDouble
- "ObjectDouble"
- else
- "Double"
- end
- end
-
- def inspected_name
- if object.instance_variable_get("@name")
- object.instance_variable_get("@name").inspect
- else
- "(anonymous)"
- end
- end
-
- def doubled_methods
- @_doubled_methods ||=
- doubled_method_names.reduce({}) do |hash, key|
- hash.merge(key => object.public_send(key))
- end
- end
-
- def doubled_method_names
- object
- .__send__(:__mock_proxy)
- .instance_variable_get("@method_doubles")
- .keys
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/generic_describable_matcher.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/generic_describable_matcher.rb
deleted file mode 100644
index 1a983ca9..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/generic_describable_matcher.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class GenericDescribableMatcher < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- ::RSpec::Matchers.is_a_describable_matcher?(value)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- t1.add_text "#<#{object.description}>"
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/hash_including.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/hash_including.rb
deleted file mode 100644
index 9cb977bf..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/hash_including.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class HashIncluding < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- SuperDiff::RSpec.a_hash_including_something?(value) ||
- SuperDiff::RSpec.hash_including_something?(value)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- # stree-ignore
- t1.as_lines_when_rendering_to_lines(
- collection_bookend: :open
- ) do |t2|
- t2.add_text "#"
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/instance_of.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/instance_of.rb
deleted file mode 100644
index e666f5f6..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/instance_of.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class InstanceOf < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- SuperDiff::RSpec.an_instance_of_something?(value) ||
- SuperDiff::RSpec.instance_of_something?(value)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- klass =
- if SuperDiff::RSpec.an_instance_of_something?(object)
- object.expected
- else
- object.instance_variable_get(:@klass)
- end
-
- t1.add_text "#"
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/kind_of.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/kind_of.rb
deleted file mode 100644
index 00ec4168..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/kind_of.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class KindOf < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- SuperDiff::RSpec.a_kind_of_something?(value) ||
- SuperDiff::RSpec.kind_of_something?(value)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- klass =
- if SuperDiff::RSpec.a_kind_of_something?(object)
- object.expected
- else
- object.instance_variable_get(:@klass)
- end
-
- t1.add_text "#"
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/object_having_attributes.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/object_having_attributes.rb
deleted file mode 100644
index 8696f07b..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/object_having_attributes.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class ObjectHavingAttributes < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- SuperDiff::RSpec.an_object_having_some_attributes?(value)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- # stree-ignore
- t1.as_lines_when_rendering_to_lines(
- collection_bookend: :open
- ) do |t2|
- t2.add_text "#"
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/primitive.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/primitive.rb
deleted file mode 100644
index d38fa296..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/primitive.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class Primitive < SuperDiff::ObjectInspection::InspectionTreeBuilders::Primitive
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/value_within.rb b/lib/super_diff/rspec/object_inspection/inspection_tree_builders/value_within.rb
deleted file mode 100644
index c9fb9854..00000000
--- a/lib/super_diff/rspec/object_inspection/inspection_tree_builders/value_within.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module SuperDiff
- module RSpec
- module ObjectInspection
- module InspectionTreeBuilders
- class ValueWithin < SuperDiff::ObjectInspection::InspectionTreeBuilders::Base
- def self.applies_to?(value)
- SuperDiff::RSpec.a_value_within_something?(value)
- end
-
- def call
- SuperDiff::ObjectInspection::InspectionTree.new do |t1|
- t1.as_prelude_when_rendering_to_lines do |t2|
- t2.add_text "#"
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/super_diff/rspec/operation_tree_builders/collection_containing_exactly.rb b/lib/super_diff/rspec/operation_tree_builders/collection_containing_exactly.rb
index a6272b90..ba95b2d8 100644
--- a/lib/super_diff/rspec/operation_tree_builders/collection_containing_exactly.rb
+++ b/lib/super_diff/rspec/operation_tree_builders/collection_containing_exactly.rb
@@ -1,7 +1,7 @@
module SuperDiff
module RSpec
module OperationTreeBuilders
- class CollectionContainingExactly < SuperDiff::OperationTreeBuilders::Base
+ class CollectionContainingExactly < Core::AbstractOperationTreeBuilder
def self.applies_to?(expected, actual)
SuperDiff::RSpec.a_collection_containing_exactly_something?(
expected
@@ -47,7 +47,7 @@ def populate_pairings_maximizer_in_expected_with(actual)
def add_noop_to(operations, index)
value = actual[index]
- operations << ::SuperDiff::Operations::UnaryOperation.new(
+ operations << Core::UnaryOperation.new(
name: :noop,
collection: collection,
key: index,
@@ -58,7 +58,7 @@ def add_noop_to(operations, index)
def add_delete_to(operations, index)
value = expected.expected[index]
- operations << ::SuperDiff::Operations::UnaryOperation.new(
+ operations << Core::UnaryOperation.new(
name: :delete,
collection: collection,
key: index,
@@ -69,7 +69,7 @@ def add_delete_to(operations, index)
def add_insert_to(operations, index)
value = actual[index]
- operations << ::SuperDiff::Operations::UnaryOperation.new(
+ operations << Core::UnaryOperation.new(
name: :insert,
collection: collection,
key: index,
diff --git a/lib/super_diff/rspec/operation_tree_builders/collection_including.rb b/lib/super_diff/rspec/operation_tree_builders/collection_including.rb
index a1bc6465..244e388a 100644
--- a/lib/super_diff/rspec/operation_tree_builders/collection_including.rb
+++ b/lib/super_diff/rspec/operation_tree_builders/collection_including.rb
@@ -1,7 +1,7 @@
module SuperDiff
module RSpec
module OperationTreeBuilders
- class CollectionIncluding < SuperDiff::OperationTreeBuilders::Array
+ class CollectionIncluding < Basic::OperationTreeBuilders::Array
def self.applies_to?(expected, actual)
(
SuperDiff::RSpec.a_collection_including_something?(expected) ||
diff --git a/lib/super_diff/rspec/operation_tree_builders/hash_including.rb b/lib/super_diff/rspec/operation_tree_builders/hash_including.rb
index b9271163..14711331 100644
--- a/lib/super_diff/rspec/operation_tree_builders/hash_including.rb
+++ b/lib/super_diff/rspec/operation_tree_builders/hash_including.rb
@@ -1,7 +1,7 @@
module SuperDiff
module RSpec
module OperationTreeBuilders
- class HashIncluding < SuperDiff::OperationTreeBuilders::Hash
+ class HashIncluding < Basic::OperationTreeBuilders::Hash
include ::RSpec::Matchers::Composable
def self.applies_to?(expected, actual)
diff --git a/lib/super_diff/rspec/operation_tree_builders/object_having_attributes.rb b/lib/super_diff/rspec/operation_tree_builders/object_having_attributes.rb
index 4b17d341..d017f8d2 100644
--- a/lib/super_diff/rspec/operation_tree_builders/object_having_attributes.rb
+++ b/lib/super_diff/rspec/operation_tree_builders/object_having_attributes.rb
@@ -1,7 +1,7 @@
module SuperDiff
module RSpec
module OperationTreeBuilders
- class ObjectHavingAttributes < SuperDiff::OperationTreeBuilders::DefaultObject
+ class ObjectHavingAttributes < Basic::OperationTreeBuilders::DefaultObject
def self.applies_to?(expected, _actual)
SuperDiff::RSpec.an_object_having_some_attributes?(expected)
end
@@ -9,7 +9,7 @@ def self.applies_to?(expected, _actual)
protected
def build_operation_tree
- find_operation_tree_for(actual)
+ SuperDiff.find_operation_tree_for(actual)
end
def attribute_names
diff --git a/lib/super_diff/tiered_lines.rb b/lib/super_diff/tiered_lines.rb
deleted file mode 100644
index d868d6f3..00000000
--- a/lib/super_diff/tiered_lines.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-module SuperDiff
- class TieredLines < Array
- end
-end
diff --git a/lib/super_diff/tiered_lines_elider.rb b/lib/super_diff/tiered_lines_elider.rb
deleted file mode 100644
index c691acf5..00000000
--- a/lib/super_diff/tiered_lines_elider.rb
+++ /dev/null
@@ -1,462 +0,0 @@
-module SuperDiff
- class TieredLinesElider
- SIZE_OF_ELISION = 1
-
- extend AttrExtras.mixin
- include Helpers
-
- method_object :lines
-
- def call
- all_lines_are_changed_or_unchanged? ? lines : elided_lines
- end
-
- private
-
- def all_lines_are_changed_or_unchanged?
- panes.size == 1 && panes.first.range == Range.new(0, lines.length - 1)
- end
-
- def elided_lines
- boxes_to_elide
- .reverse
- .reduce(lines) do |lines_with_elisions, box|
- with_box_elided(box, lines_with_elisions)
- end
- end
-
- def boxes_to_elide
- @_boxes_to_elide ||=
- panes_to_consider_for_eliding.reduce([]) do |array, pane|
- array + (find_boxes_to_elide_within(pane) || [])
- end
- end
-
- def panes_to_consider_for_eliding
- panes.select { |pane| pane.type == :clean && pane.range.size > maximum }
- end
-
- def panes
- @_panes ||= BuildPanes.call(dirty_panes: padded_dirty_panes, lines: lines)
- end
-
- def padded_dirty_panes
- @_padded_dirty_panes ||=
- combine_congruent_panes(
- dirty_panes
- .map(&:padded)
- .map { |pane| pane.capped_to(0, lines.size - 1) }
- )
- end
-
- def dirty_panes
- @_dirty_panes ||=
- lines
- .each_with_index
- .select { |line, index| line.type != :noop }
- .reduce([]) do |panes, (_, index)|
- if !panes.empty? && panes.last.range.end == index - 1
- panes[0..-2] + [panes[-1].extended_to(index)]
- else
- panes + [Pane.new(type: :dirty, range: index..index)]
- end
- end
- end
-
- def with_box_elided(box, lines)
- box_at_start_of_lines =
- if lines.first.complete_bookend?
- box.range.begin == 1
- else
- box.range.begin == 0
- end
-
- box_at_end_of_lines =
- if lines.last.complete_bookend?
- box.range.end == lines.size - 2
- else
- box.range.end == lines.size - 1
- end
-
- if one_dimensional_line_tree? && outermost_box?(box)
- if box_at_start_of_lines
- with_start_of_box_elided(box, lines)
- elsif box_at_end_of_lines
- with_end_of_box_elided(box, lines)
- else
- with_middle_of_box_elided(box, lines)
- end
- else
- with_subset_of_lines_elided(
- lines,
- range: box.range,
- indentation_level: box.indentation_level
- )
- end
- end
-
- def outermost_box?(box)
- box.indentation_level == all_indentation_levels.min
- end
-
- def one_dimensional_line_tree?
- all_indentation_levels.size == 1
- end
-
- def all_indentation_levels
- lines
- .map(&:indentation_level)
- .select { |indentation_level| indentation_level > 0 }
- .uniq
- end
-
- def find_boxes_to_elide_within(pane)
- set_of_boxes =
- normalized_box_groups_at_decreasing_indentation_levels_within(pane)
-
- total_size_before_eliding =
- lines[pane.range].reject(&:complete_bookend?).size
-
- if total_size_before_eliding > maximum
- if maximum > 0
- set_of_boxes.find do |boxes|
- total_size_after_eliding =
- total_size_before_eliding -
- boxes.sum { |box| box.range.size - SIZE_OF_ELISION }
- total_size_after_eliding <= maximum
- end
- else
- set_of_boxes[-1]
- end
- else
- []
- end
- end
-
- def normalized_box_groups_at_decreasing_indentation_levels_within(pane)
- box_groups_at_decreasing_indentation_levels_within(pane).map(
- &method(:filter_out_boxes_fully_contained_in_others)
- ).map(&method(:combine_congruent_boxes))
- end
-
- def box_groups_at_decreasing_indentation_levels_within(pane)
- boxes_within_pane = boxes.select { |box| box.fits_fully_within?(pane) }
-
- possible_indentation_levels =
- boxes_within_pane
- .map(&:indentation_level)
- .select { |indentation_level| indentation_level > 0 }
- .uniq
- .sort
- .reverse
-
- possible_indentation_levels.map do |indentation_level|
- boxes_within_pane.select do |box|
- box.indentation_level >= indentation_level
- end
- end
- end
-
- def filter_out_boxes_fully_contained_in_others(boxes)
- sorted_boxes =
- boxes.sort_by do |box|
- [box.indentation_level, box.range.begin, box.range.end]
- end
-
- boxes.reject do |box2|
- sorted_boxes.any? do |box1|
- !box1.equal?(box2) && box1.fully_contains?(box2)
- end
- end
- end
-
- def combine_congruent_boxes(boxes)
- combine(boxes, on: :indentation_level)
- end
-
- def combine_congruent_panes(panes)
- combine(panes, on: :type)
- end
-
- def combine(spannables, on:)
- criterion = on
- spannables.reduce([]) do |combined_spannables, spannable|
- if (
- !combined_spannables.empty? &&
- spannable.range.begin <=
- combined_spannables.last.range.end + 1 &&
- spannable.public_send(criterion) ==
- combined_spannables.last.public_send(criterion)
- )
- combined_spannables[0..-2] +
- [combined_spannables[-1].extended_to(spannable.range.end)]
- else
- combined_spannables + [spannable]
- end
- end
- end
-
- def boxes
- @_boxes ||= BuildBoxes.call(lines)
- end
-
- def with_start_of_box_elided(box, lines)
- amount_to_elide =
- if maximum > 0
- box.range.size - maximum + SIZE_OF_ELISION
- else
- box.range.size
- end
-
- with_subset_of_lines_elided(
- lines,
- range:
- Range.new(box.range.begin, box.range.begin + amount_to_elide - 1),
- indentation_level: box.indentation_level
- )
- end
-
- def with_end_of_box_elided(box, lines)
- amount_to_elide =
- if maximum > 0
- box.range.size - maximum + SIZE_OF_ELISION
- else
- box.range.size
- end
-
- range =
- if amount_to_elide > 0
- Range.new(box.range.end - amount_to_elide + 1, box.range.end)
- else
- box.range
- end
-
- with_subset_of_lines_elided(
- lines,
- range: range,
- indentation_level: box.indentation_level
- )
- end
-
- def with_middle_of_box_elided(box, lines)
- half_of_maximum, remainder =
- if maximum > 0
- (maximum - SIZE_OF_ELISION).divmod(2)
- else
- [0, 0]
- end
-
- opening_length, closing_length =
- half_of_maximum,
- half_of_maximum + remainder
-
- with_subset_of_lines_elided(
- lines,
- range:
- Range.new(
- box.range.begin + opening_length,
- box.range.end - closing_length
- ),
- indentation_level: box.indentation_level
- )
- end
-
- def with_subset_of_lines_elided(lines, range:, indentation_level:)
- with_slice_of_array_replaced(
- lines,
- range,
- Elision.new(
- indentation_level: indentation_level,
- children: lines[range].map(&:as_elided)
- )
- )
- end
-
- def maximum
- SuperDiff.configuration.diff_elision_maximum || 0
- end
-
- class BuildPanes
- extend AttrExtras.mixin
-
- method_object %i[dirty_panes! lines!]
-
- def call
- beginning + middle + ending
- end
-
- private
-
- def beginning
- if (dirty_panes.empty? || dirty_panes.first.range.begin == 0)
- []
- else
- [
- Pane.new(
- type: :clean,
- range: Range.new(0, dirty_panes.first.range.begin - 1)
- )
- ]
- end
- end
-
- def middle
- if dirty_panes.size == 1
- dirty_panes
- else
- dirty_panes
- .each_with_index
- .each_cons(2)
- .reduce([]) do |panes, ((pane1, _), (pane2, index2))|
- panes +
- [
- pane1,
- Pane.new(
- type: :clean,
- range: Range.new(pane1.range.end + 1, pane2.range.begin - 1)
- )
- ] + (index2 == dirty_panes.size - 1 ? [pane2] : [])
- end
- end
- end
-
- def ending
- if (dirty_panes.empty? || dirty_panes.last.range.end >= lines.size - 1)
- []
- else
- [
- Pane.new(
- type: :clean,
- range: Range.new(dirty_panes.last.range.end + 1, lines.size - 1)
- )
- ]
- end
- end
- end
-
- class Pane
- extend AttrExtras.mixin
-
- rattr_initialize %i[type! range!]
-
- def extended_to(new_end)
- self.class.new(type: type, range: range.begin..new_end)
- end
-
- def padded
- self.class.new(type: type, range: Range.new(range.begin, range.end))
- end
-
- def capped_to(beginning, ending)
- new_beginning = range.begin < beginning ? beginning : range.begin
- new_ending = range.end > ending ? ending : range.end
- self.class.new(type: type, range: Range.new(new_beginning, new_ending))
- end
- end
-
- class BuildBoxes
- def self.call(lines)
- builder = new(lines)
- builder.build
- builder.final_boxes
- end
-
- attr_reader :final_boxes
-
- def initialize(lines)
- @lines = lines
-
- @open_collection_boxes = []
- @final_boxes = []
- end
-
- def build
- lines.each_with_index do |line, index|
- if line.opens_collection?
- open_new_collection_box(line, index)
- elsif line.closes_collection?
- extend_working_collection_box(index)
- close_working_collection_box
- else
- extend_working_collection_box(index) if open_collection_boxes.any?
- record_item_box(line, index)
- end
- end
- end
-
- private
-
- attr_reader :lines, :open_collection_boxes
-
- def extend_working_collection_box(index)
- open_collection_boxes.last.extend_to(index)
- end
-
- def close_working_collection_box
- final_boxes << open_collection_boxes.pop
- end
-
- def open_new_collection_box(line, index)
- open_collection_boxes << Box.new(
- indentation_level: line.indentation_level,
- range: index..index
- )
- end
-
- def record_item_box(line, index)
- final_boxes << Box.new(
- indentation_level: line.indentation_level,
- range: index..index
- )
- end
- end
-
- class Box
- extend AttrExtras.mixin
-
- rattr_initialize %i[indentation_level! range!]
-
- def fully_contains?(other)
- range.begin <= other.range.begin && range.end >= other.range.end
- end
-
- def fits_fully_within?(other)
- other.range.begin <= range.begin && other.range.end >= range.end
- end
-
- def extended_to(new_end)
- dup.tap { |clone| clone.extend_to(new_end) }
- end
-
- def extend_to(new_end)
- @range = range.begin..new_end
- end
- end
-
- class Elision
- extend AttrExtras.mixin
-
- rattr_initialize %i[indentation_level! children!]
-
- def type
- :elision
- end
-
- def prefix
- ""
- end
-
- def value
- "# ..."
- end
-
- def elided?
- true
- end
-
- def add_comma?
- false
- end
- end
- end
-end
diff --git a/lib/super_diff/tiered_lines_formatter.rb b/lib/super_diff/tiered_lines_formatter.rb
deleted file mode 100644
index 9a21a013..00000000
--- a/lib/super_diff/tiered_lines_formatter.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-module SuperDiff
- class TieredLinesFormatter
- extend AttrExtras.mixin
-
- method_object :tiered_lines
-
- def call
- colorized_document.to_s.chomp
- end
-
- private
-
- def colorized_document
- SuperDiff::Helpers.style do |doc|
- formattable_lines.each do |formattable_line|
- doc.public_send(
- "#{formattable_line.color}_line",
- formattable_line.content
- )
- end
- end
- end
-
- def formattable_lines
- tiered_lines.map { |line| FormattableLine.new(line) }
- end
-
- class FormattableLine
- extend AttrExtras.mixin
-
- INDENTATION_UNIT = " ".freeze
- ICONS = { delete: "-", insert: "+", elision: " ", noop: " " }.freeze
- COLORS = {
- delete: :expected,
- insert: :actual,
- elision: :elision_marker,
- noop: :plain
- }.freeze
-
- pattr_initialize :line
-
- def content
- icon + " " + indentation + line.prefix + line.value + possible_comma
- end
-
- def color
- COLORS.fetch(line.type) do
- raise(
- KeyError,
- "Couldn't find color for line type #{line.type.inspect}!"
- )
- end
- end
-
- private
-
- def icon
- ICONS.fetch(line.type) do
- raise(
- KeyError,
- "Couldn't find icon for line type #{line.type.inspect}!"
- )
- end
- end
-
- def indentation
- INDENTATION_UNIT * line.indentation_level
- end
-
- def possible_comma
- line.add_comma? ? "," : ""
- end
- end
- end
-end
diff --git a/lib/super_diff/version.rb b/lib/super_diff/version.rb
index 8f77097b..90a130e0 100644
--- a/lib/super_diff/version.rb
+++ b/lib/super_diff/version.rb
@@ -1,3 +1,3 @@
module SuperDiff
- VERSION = "0.11.0".freeze
+ VERSION = "0.12.1".freeze
end
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..5b27ef76
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,53 @@
+site_name: SuperDiff Documentation
+site_description: User- and contributor-facing documentation for SuperDiff
+copyright: ÂŠī¸ Elliot Winkler.
+
+# NOTE: Don't do this for now as it loads the version and stars dynamically
+#repo_url: https://github.com/mcmire/super_diff
+#repo_name: SuperDiff on GitHub
+
+theme:
+ name: material
+ features:
+ - navigation.instant
+ - navigation.instant.progress
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.path
+ - toc.integrate
+ #plugins:
+ #- search
+
+nav:
+ - Home: index.md
+ - User Documentation:
+ - "Introduction to SuperDiff": "users/index.md"
+ - "Getting Started": "users/getting-started.md"
+ - "Customizing SuperDiff": "users/customization.md"
+ - Contributor Documentation:
+ - "Home": "contributors/index.md"
+ - "How to Contribute": "contributors/how-to-contribute.md"
+ - Architecture:
+ - "Introduction": "contributors/architecture/introduction.md"
+ - "How RSpec works": "contributors/architecture/how-rspec-works.md"
+ - "How SuperDiff works": "contributors/architecture/how-super-diff-works.md"
+ - "Structure": "contributors/architecture/structure.md"
+
+#plugins:
+#- entangled # this also runs `entangled sync` as a pre-build action
+
+extra_css:
+ - stylesheets/extra.css
+
+markdown_extensions:
+ - admonition
+ - def_list
+ - footnotes
+ - smarty
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+ - pymdownx.tabbed:
+ alternate_style: true
diff --git a/package.json b/package.json
index 49d1e95a..228e1c4f 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,10 @@
"private": true,
"scripts": {
"audit": "yarn npm audit && bundle exec bundle audit --gemfile-lock ${BUNDLE_GEMFILE:-Gemfile}",
- "lint": "prettier --check .",
- "lint:fix": "yarn lint --write",
+ "lint": "scripts/lint-all-files.sh --check",
+ "lint:fix": "scripts/lint-all-files.sh --write",
+ "lint:changed": "scripts/lint-changed-files.sh --include-uncommitted --check",
+ "lint:changed:fix": "scripts/lint-changed-files.sh --include-uncommitted --write",
"setup-git-hooks": "husky install"
},
"devDependencies": {
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..94cb9988
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,17 @@
+[tool.poetry]
+name = "super-diff"
+version = "0.0.0"
+description = "A more helpful way to view differences between complex data structures in RSpec"
+authors = ["Elliot Winkler "]
+license = "MIT"
+readme = "README.md"
+
+[tool.poetry.group.dev.dependencies]
+python = "^3.12"
+mkdocs = { version = "^1.5.3", python = ">=3.7" }
+mkdocs-material = { version = "^9.5.9", python = ">=3.8" }
+
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/scripts/collect-release-info.rb b/scripts/collect-release-info.rb
new file mode 100755
index 00000000..f74a8179
--- /dev/null
+++ b/scripts/collect-release-info.rb
@@ -0,0 +1,19 @@
+#!/usr/bin/env ruby
+
+github_output = File.open(ENV.fetch("GITHUB_OUTPUT"), "a")
+
+spec = Gem::Specification.load("super_diff.gemspec")
+current_version = spec.version
+
+latest_version = Gem.latest_version_for("super_diff")
+
+puts "Current version is #{current_version}, latest version is #{latest_version}"
+
+if current_version == latest_version
+ puts "This isn't a new release."
+ github_output.puts("IS_NEW_RELEASE=false")
+else
+ puts "Looks like a new release!"
+ github_output.puts("IS_NEW_RELEASE=true")
+ github_output.puts("RELEASE_VERSION=#{current_version}")
+end
diff --git a/scripts/lint-all-files.sh b/scripts/lint-all-files.sh
new file mode 100755
index 00000000..b4fc85e1
--- /dev/null
+++ b/scripts/lint-all-files.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+exec prettier "$@" .
diff --git a/scripts/lint-changed-files.sh b/scripts/lint-changed-files.sh
new file mode 100755
index 00000000..0bd73c14
--- /dev/null
+++ b/scripts/lint-changed-files.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+get-files-to-lint() {
+ local flag="$1"
+ local current_branch_name="$(git branch --show-current)"
+
+ if [[ "$current_branch_name" == "main" ]]; then
+ git diff origin/main...HEAD --name-only --diff-filter=d
+ else
+ git diff main...HEAD --name-only --diff-filter=d
+ fi
+
+ if [[ $flag == "--include-uncommitted" ]]; then
+ git diff --name-only --diff-filter=d
+ fi
+}
+
+main() {
+ local prettier_flag
+ local include_uncommitted_flag
+
+ while [[ -n "$1" ]]; do
+ case "$1" in
+ --check | --write)
+ prettier_flag="$1"
+ shift
+ ;;
+ --include-uncommitted)
+ include_uncommitted_flag="$1"
+ shift
+ ;;
+ *)
+ echo "ERROR: Unknown option $1."
+ exit 1
+ ;;
+ esac
+ done
+
+ local files_to_lint="$(get-files-to-lint "$include_uncommitted_flag")"
+
+ if [[ -z "$prettier_flag" ]]; then
+ echo "ERROR: Missing --check or --write."
+ exit 1
+ fi
+
+ echo "*** Checking for lint violations in changed files ***************"
+ echo
+
+ if [[ -n "$files_to_lint" ]]; then
+ echo "Files to check:"
+ echo "$files_to_lint" | while IFS=$'\n' read -r line; do
+ echo "- $line"
+ done
+
+ echo
+ echo "$files_to_lint" | while IFS=$'\n' read -r line; do
+ printf '%s\0' "$line"
+ done | xargs -0 yarn prettier "$prettier_flag" --ignore-unknown
+ else
+ echo "No files to lint this time."
+ fi
+}
+
+main "$@"
diff --git a/spec/support/integration/helpers.rb b/spec/support/integration/helpers.rb
index d498224d..33d18cb6 100644
--- a/spec/support/integration/helpers.rb
+++ b/spec/support/integration/helpers.rb
@@ -115,7 +115,10 @@ def build_expected_output(
end
def colored(color_enabled: true, &block)
- SuperDiff::Helpers.style(color_enabled: color_enabled, &block).to_s.chomp
+ SuperDiff::Core::Helpers
+ .style(color_enabled: color_enabled, &block)
+ .to_s
+ .chomp
end
end
end
diff --git a/spec/support/integration/matchers/produce_output_when_run_matcher.rb b/spec/support/integration/matchers/produce_output_when_run_matcher.rb
index 66859e40..43d3a9fd 100644
--- a/spec/support/integration/matchers/produce_output_when_run_matcher.rb
+++ b/spec/support/integration/matchers/produce_output_when_run_matcher.rb
@@ -12,7 +12,7 @@ def initialize(expected_output)
end
def removing_object_ids
- first_replacing(/#<([\w:]+):0x[a-f0-9]+/, '#<\1')
+ first_replacing(/#<([\w_:]+):0x[a-f0-9]+/, '#<\1')
self
end
diff --git a/spec/support/models/active_record/person.rb b/spec/support/models/active_record/person.rb
index bf8ec2c5..c86a31e7 100644
--- a/spec/support/models/active_record/person.rb
+++ b/spec/support/models/active_record/person.rb
@@ -3,6 +3,7 @@ module Test
module Models
module ActiveRecord
class Person < ::ActiveRecord::Base
+ self.primary_key = "person_id"
end
end
end
@@ -13,7 +14,13 @@ class Person < ::ActiveRecord::Base
config.before do
ActiveRecord::Base
.connection
- .create_table(:people, force: true) do |t|
+ .create_table(
+ :people,
+ id: false,
+ primary_key: "person_id",
+ force: true
+ ) do |t|
+ t.primary_key :person_id, null: false
t.string :name, null: false
t.integer :age, null: false
end
diff --git a/spec/support/shared_examples/active_record.rb b/spec/support/shared_examples/active_record.rb
index 076c0e88..4a172f4e 100644
--- a/spec/support/shared_examples/active_record.rb
+++ b/spec/support/shared_examples/active_record.rb
@@ -86,7 +86,7 @@
proc do
line do
plain "Expected "
- actual %|#|
+ actual %|#|
end
line do
@@ -244,7 +244,7 @@
proc do
line do
plain "Expected "
- actual %|{ name: "Marty McFly", shipping_address: # }|
+ actual %|{ name: "Marty McFly", shipping_address: # }|
end
line do
@@ -265,7 +265,7 @@
expected_line %|- zip: "90382"|
expected_line "- }>"
actual_line "+ shipping_address: #"
@@ -390,7 +390,7 @@
proc do
line do
plain "Expected "
- actual %|[#]>>]|
+ actual %|[#]>>]|
end
line do
@@ -404,7 +404,7 @@
plain_line " #)
+ %(#)
)
end
end
@@ -42,7 +42,7 @@
an_object_having_attributes(
type: :delete,
indentation_level: 2,
- prefix: "id: ",
+ prefix: "person_id: ",
value: "nil",
add_comma: true
),
@@ -91,7 +91,7 @@
)
expect(string).to eq(
- %(#, #]>)
+ %(#, #]>)
)
end
end
@@ -132,7 +132,7 @@
an_object_having_attributes(
type: :delete,
indentation_level: 3,
- prefix: "id: ",
+ prefix: "person_id: ",
value: "1",
add_comma: true
),
@@ -165,7 +165,7 @@
an_object_having_attributes(
type: :delete,
indentation_level: 3,
- prefix: "id: ",
+ prefix: "person_id: ",
value: "2",
add_comma: true
),
diff --git a/spec/unit/operation_tree_flatteners/array_spec.rb b/spec/unit/basic/operation_tree_flatteners/array_spec.rb
similarity index 97%
rename from spec/unit/operation_tree_flatteners/array_spec.rb
rename to spec/unit/basic/operation_tree_flatteners/array_spec.rb
index 0a239ebe..1b43f6b6 100644
--- a/spec/unit/operation_tree_flatteners/array_spec.rb
+++ b/spec/unit/basic/operation_tree_flatteners/array_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-RSpec.describe SuperDiff::OperationTreeFlatteners::Array do
+RSpec.describe SuperDiff::Basic::OperationTreeFlatteners::Array do
context "given an empty tree" do
it "returns a set of lines which are simply the open token and close token" do
expect(described_class.call([])).to match(
@@ -30,7 +30,7 @@
it "returns a series of lines from inspecting each value, creating multiple lines upon encountering inner data structures" do
collection = Array.new(3) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::Array.new(
+ SuperDiff::Basic::OperationTrees::Array.new(
[
double(
:operation,
@@ -134,7 +134,7 @@
expected = Array.new(3) { :some_value }
actual = Array.new(4) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::Array.new(
+ SuperDiff::Basic::OperationTrees::Array.new(
[
double(
:operation,
@@ -266,7 +266,7 @@
collection = Array.new(3) { :some_value }
subcollection = Array.new(2) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::Array.new(
+ SuperDiff::Basic::OperationTrees::Array.new(
[
double(
:operation,
@@ -283,7 +283,7 @@
right_collection: collection,
right_index: 1,
children:
- SuperDiff::OperationTrees::Array.new(
+ SuperDiff::Basic::OperationTrees::Array.new(
[
double(
:operation,
@@ -406,7 +406,7 @@
right_collection << right_collection
operation_tree =
- SuperDiff::OperationTrees::Array.new(
+ SuperDiff::Basic::OperationTrees::Array.new(
[
double(
:operation,
@@ -502,7 +502,7 @@
right_subcollection << right_subcollection
operation_tree =
- SuperDiff::OperationTrees::Array.new(
+ SuperDiff::Basic::OperationTrees::Array.new(
[
double(
:operation,
@@ -519,7 +519,7 @@
right_collection: collection,
right_index: 1,
children:
- SuperDiff::OperationTrees::Array.new(
+ SuperDiff::Basic::OperationTrees::Array.new(
[
double(
:operation,
diff --git a/spec/unit/operation_tree_flatteners/custom_object_spec.rb b/spec/unit/basic/operation_tree_flatteners/custom_object_spec.rb
similarity index 97%
rename from spec/unit/operation_tree_flatteners/custom_object_spec.rb
rename to spec/unit/basic/operation_tree_flatteners/custom_object_spec.rb
index 9dbcfa8e..0c8a7d19 100644
--- a/spec/unit/operation_tree_flatteners/custom_object_spec.rb
+++ b/spec/unit/basic/operation_tree_flatteners/custom_object_spec.rb
@@ -1,10 +1,10 @@
require "spec_helper"
-RSpec.describe SuperDiff::OperationTreeFlatteners::CustomObject do
+RSpec.describe SuperDiff::Basic::OperationTreeFlatteners::CustomObject do
context "given an empty tree" do
it "returns a set of lines which are simply the open token and close token" do
operation_tree =
- SuperDiff::OperationTrees::CustomObject.new(
+ SuperDiff::Basic::OperationTrees::CustomObject.new(
[],
underlying_object: underlying_object
)
@@ -38,7 +38,7 @@
it "returns a series of lines from inspecting each value, creating multiple lines upon encountering inner data structures" do
collection = Array.new(3) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::CustomObject.new(
+ SuperDiff::Basic::OperationTrees::CustomObject.new(
[
double(
:operation,
@@ -146,7 +146,7 @@
expected = Array.new(3) { :some_value }
actual = Array.new(4) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::CustomObject.new(
+ SuperDiff::Basic::OperationTrees::CustomObject.new(
[
double(
:operation,
@@ -284,7 +284,7 @@
collection = Array.new(3) { :some_value }
subcollection = Array.new(2) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::CustomObject.new(
+ SuperDiff::Basic::OperationTrees::CustomObject.new(
[
double(
:operation,
@@ -304,7 +304,7 @@
right_key: :baz,
right_index: 1,
children:
- SuperDiff::OperationTrees::CustomObject.new(
+ SuperDiff::Basic::OperationTrees::CustomObject.new(
[
double(
:operation,
@@ -435,7 +435,7 @@
.tap { |collection| collection << right_collection }
operation_tree =
- SuperDiff::OperationTrees::CustomObject.new(
+ SuperDiff::Basic::OperationTrees::CustomObject.new(
[
double(
:operation,
@@ -536,7 +536,7 @@
Array.new(1) { :some_value }.tap { |coll| coll << right_subcollection }
operation_tree =
- SuperDiff::OperationTrees::CustomObject.new(
+ SuperDiff::Basic::OperationTrees::CustomObject.new(
[
double(
:operation,
@@ -556,7 +556,7 @@
right_key: :baz,
right_index: 1,
children:
- SuperDiff::OperationTrees::CustomObject.new(
+ SuperDiff::Basic::OperationTrees::CustomObject.new(
[
double(
:operation,
diff --git a/spec/unit/operation_tree_flatteners/default_object_spec.rb b/spec/unit/basic/operation_tree_flatteners/default_object_spec.rb
similarity index 97%
rename from spec/unit/operation_tree_flatteners/default_object_spec.rb
rename to spec/unit/basic/operation_tree_flatteners/default_object_spec.rb
index df396459..43e082fa 100644
--- a/spec/unit/operation_tree_flatteners/default_object_spec.rb
+++ b/spec/unit/basic/operation_tree_flatteners/default_object_spec.rb
@@ -1,10 +1,10 @@
require "spec_helper"
-RSpec.describe SuperDiff::OperationTreeFlatteners::DefaultObject do
+RSpec.describe SuperDiff::Basic::OperationTreeFlatteners::DefaultObject do
context "given an empty tree" do
it "returns a set of lines which are simply the open token and close token" do
operation_tree =
- SuperDiff::OperationTrees::DefaultObject.new(
+ SuperDiff::Basic::OperationTrees::DefaultObject.new(
[],
underlying_object: underlying_object
)
@@ -38,7 +38,7 @@
it "returns a series of lines from inspecting each value, creating multiple lines upon encountering inner data structures" do
collection = Array.new(3) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::DefaultObject.new(
+ SuperDiff::Basic::OperationTrees::DefaultObject.new(
[
double(
:operation,
@@ -146,7 +146,7 @@
expected = Array.new(3) { :some_value }
actual = Array.new(4) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::DefaultObject.new(
+ SuperDiff::Basic::OperationTrees::DefaultObject.new(
[
double(
:operation,
@@ -284,7 +284,7 @@
collection = Array.new(3) { :some_value }
subcollection = Array.new(2) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::DefaultObject.new(
+ SuperDiff::Basic::OperationTrees::DefaultObject.new(
[
double(
:operation,
@@ -304,7 +304,7 @@
right_key: :baz,
right_index: 1,
children:
- SuperDiff::OperationTrees::DefaultObject.new(
+ SuperDiff::Basic::OperationTrees::DefaultObject.new(
[
double(
:operation,
@@ -435,7 +435,7 @@
.tap { |collection| collection << right_collection }
operation_tree =
- SuperDiff::OperationTrees::DefaultObject.new(
+ SuperDiff::Basic::OperationTrees::DefaultObject.new(
[
double(
:operation,
@@ -536,7 +536,7 @@
Array.new(1) { :some_value }.tap { |coll| coll << right_subcollection }
operation_tree =
- SuperDiff::OperationTrees::DefaultObject.new(
+ SuperDiff::Basic::OperationTrees::DefaultObject.new(
[
double(
:operation,
@@ -556,7 +556,7 @@
right_key: :baz,
right_index: 1,
children:
- SuperDiff::OperationTrees::DefaultObject.new(
+ SuperDiff::Basic::OperationTrees::DefaultObject.new(
[
double(
:operation,
diff --git a/spec/unit/operation_tree_flatteners/hash_spec.rb b/spec/unit/basic/operation_tree_flatteners/hash_spec.rb
similarity index 97%
rename from spec/unit/operation_tree_flatteners/hash_spec.rb
rename to spec/unit/basic/operation_tree_flatteners/hash_spec.rb
index d8eaf075..6db948a7 100644
--- a/spec/unit/operation_tree_flatteners/hash_spec.rb
+++ b/spec/unit/basic/operation_tree_flatteners/hash_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-RSpec.describe SuperDiff::OperationTreeFlatteners::Hash do
+RSpec.describe SuperDiff::Basic::OperationTreeFlatteners::Hash do
context "given an empty tree" do
it "returns a set of lines which are simply the open token and close token" do
expect(described_class.call([])).to match(
@@ -30,7 +30,7 @@
it "returns a series of lines from inspecting each value, creating multiple lines upon encountering inner data structures" do
collection = Array.new(3) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::Hash.new(
+ SuperDiff::Basic::OperationTrees::Hash.new(
[
double(
:operation,
@@ -140,7 +140,7 @@
expected = Array.new(3) { :some_value }
actual = Array.new(4) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::Hash.new(
+ SuperDiff::Basic::OperationTrees::Hash.new(
[
double(
:operation,
@@ -280,7 +280,7 @@
collection = Array.new(3) { :some_value }
subcollection = Array.new(2) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::Hash.new(
+ SuperDiff::Basic::OperationTrees::Hash.new(
[
double(
:operation,
@@ -300,7 +300,7 @@
right_key: :baz,
right_index: 1,
children:
- SuperDiff::OperationTrees::Hash.new(
+ SuperDiff::Basic::OperationTrees::Hash.new(
[
double(
:operation,
@@ -429,7 +429,7 @@
.tap { |collection| collection << right_collection }
operation_tree =
- SuperDiff::OperationTrees::Hash.new(
+ SuperDiff::Basic::OperationTrees::Hash.new(
[
double(
:operation,
@@ -529,7 +529,7 @@
Array.new(1) { :some_value }.tap { |coll| coll << right_subcollection }
operation_tree =
- SuperDiff::OperationTrees::Hash.new(
+ SuperDiff::Basic::OperationTrees::Hash.new(
[
double(
:operation,
@@ -549,7 +549,7 @@
right_key: :baz,
right_index: 1,
children:
- SuperDiff::OperationTrees::Hash.new(
+ SuperDiff::Basic::OperationTrees::Hash.new(
[
double(
:operation,
diff --git a/spec/unit/operation_tree_flatteners/multiline_string_spec.rb b/spec/unit/basic/operation_tree_flatteners/multiline_string_spec.rb
similarity index 92%
rename from spec/unit/operation_tree_flatteners/multiline_string_spec.rb
rename to spec/unit/basic/operation_tree_flatteners/multiline_string_spec.rb
index ab904940..2eeae0d3 100644
--- a/spec/unit/operation_tree_flatteners/multiline_string_spec.rb
+++ b/spec/unit/basic/operation_tree_flatteners/multiline_string_spec.rb
@@ -1,9 +1,9 @@
require "spec_helper"
-RSpec.describe SuperDiff::OperationTreeFlatteners::MultilineString do
+RSpec.describe SuperDiff::Basic::OperationTreeFlatteners::MultilineString do
context "given an empty tree" do
it "returns an empty set of lines" do
- operation_tree = SuperDiff::OperationTrees::MultilineString.new([])
+ operation_tree = SuperDiff::Basic::OperationTrees::MultilineString.new([])
flattened_operation_tree = described_class.call(operation_tree)
expect(flattened_operation_tree).to eq([])
end
@@ -13,7 +13,7 @@
it "returns a series of lines from printing each value" do
collection = Array.new(3) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::MultilineString.new(
+ SuperDiff::Basic::OperationTrees::MultilineString.new(
[
double(
:operation,
@@ -73,7 +73,7 @@
it "returns a series of lines from printing each value" do
collection = Array.new(3) { :some_value }
operation_tree =
- SuperDiff::OperationTrees::MultilineString.new(
+ SuperDiff::Basic::OperationTrees::MultilineString.new(
[
double(
:operation,
diff --git a/spec/unit/helpers_spec.rb b/spec/unit/core/helpers_spec.rb
similarity index 94%
rename from spec/unit/helpers_spec.rb
rename to spec/unit/core/helpers_spec.rb
index 20dc52d6..18a0d479 100644
--- a/spec/unit/helpers_spec.rb
+++ b/spec/unit/core/helpers_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-RSpec.describe SuperDiff::Helpers do
+RSpec.describe SuperDiff::Core::Helpers do
shared_examples "helper tests" do
describe "with_slice_of_array_replaced" do
context "if the given range covers the whole array" do
@@ -32,7 +32,7 @@
end
describe "object_address_for" do
- if SuperDiff::Helpers.ruby_version_matches?(">= 2.7.0")
+ if SuperDiff::Core::Helpers.ruby_version_matches?(">= 2.7.0")
it "returns an empty string for Floats" do
expect(helper.object_address_for(1.0)).to eq("")
end
diff --git a/spec/unit/tiered_lines_elider_spec.rb b/spec/unit/core/tiered_lines_elider_spec.rb
similarity index 99%
rename from spec/unit/tiered_lines_elider_spec.rb
rename to spec/unit/core/tiered_lines_elider_spec.rb
index f61e7352..8cfa9be0 100644
--- a/spec/unit/tiered_lines_elider_spec.rb
+++ b/spec/unit/core/tiered_lines_elider_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-RSpec.describe SuperDiff::TieredLinesElider, type: :unit do
+RSpec.describe SuperDiff::Core::TieredLinesElider, type: :unit do
context "and the gem is configured with :diff_elision_maximum" do
context "and :diff_elision_maximum is more than 0" do
context "and the line tree contains a section of noops that does not span more than the maximum" do
@@ -6299,7 +6299,7 @@
def an_actual_line(**args)
add_comma = args.delete(:add_comma?) { false }
- SuperDiff::Line.new(**args, add_comma: add_comma)
+ SuperDiff::Core::Line.new(**args, add_comma: add_comma)
end
def an_expected_line(type:, indentation_level:, value:, children: [], **rest)
diff --git a/spec/unit/tiered_lines_formatter_spec.rb b/spec/unit/core/tiered_lines_formatter_spec.rb
similarity index 98%
rename from spec/unit/tiered_lines_formatter_spec.rb
rename to spec/unit/core/tiered_lines_formatter_spec.rb
index e7d39641..f9496e18 100644
--- a/spec/unit/tiered_lines_formatter_spec.rb
+++ b/spec/unit/core/tiered_lines_formatter_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-RSpec.describe SuperDiff::TieredLinesFormatter, type: :unit do
+RSpec.describe SuperDiff::Core::TieredLinesFormatter, type: :unit do
it "formats the given lines as an array of strings with appropriate colors and indentation" do
tiered_lines = [
line(type: :noop, indentation_level: 0, value: "["),
diff --git a/spec/unit/deprecations_spec.rb b/spec/unit/deprecations_spec.rb
new file mode 100644
index 00000000..5bb43f71
--- /dev/null
+++ b/spec/unit/deprecations_spec.rb
@@ -0,0 +1,176 @@
+require "spec_helper"
+
+# stree-ignore
+common_constant_remappings = {
+ "SuperDiff::ColorizedDocumentExtensions" => "SuperDiff::Core::ColorizedDocumentExtensions",
+ "SuperDiff::Configuration" => "SuperDiff::Core::Configuration",
+ # TODO: Add back?
+ # "SuperDiff::DiffFormatters::Collection" => "SuperDiff::Basic::DiffFormatters::Collection",
+ # "SuperDiff::DiffFormatters::MultilineString" => "SuperDiff::Basic::DiffFormatters::MultilineString",
+ "SuperDiff::Differs::Array" => "SuperDiff::Basic::Differs::Array",
+ "SuperDiff::Differs::Base" => "SuperDiff::Core::AbstractDiffer",
+ "SuperDiff::Differs::CustomObject" => "SuperDiff::Basic::Differs::CustomObject",
+ "SuperDiff::Differs::DateLike" => "SuperDiff::Basic::Differs::DateLike",
+ "SuperDiff::Differs::DefaultObject" => "SuperDiff::Basic::Differs::DefaultObject",
+ "SuperDiff::Differs::Hash" => "SuperDiff::Basic::Differs::Hash",
+ "SuperDiff::Differs::MultilineString" => "SuperDiff::Basic::Differs::MultilineString",
+ "SuperDiff::Differs::TimeLike" => "SuperDiff::Basic::Differs::TimeLike",
+ "SuperDiff::Errors::NoDifferAvailableError" => "SuperDiff::Core::NoDifferAvailableError",
+ "SuperDiff::GemVersion" => "SuperDiff::Core::GemVersion",
+ "SuperDiff::Helpers" => "SuperDiff::Core::Helpers",
+ "SuperDiff::ImplementationChecks" => "SuperDiff::Core::ImplementationChecks",
+ "SuperDiff::Line" => "SuperDiff::Core::Line",
+ "SuperDiff::ObjectInspection::InspectionTree" => "SuperDiff::Core::InspectionTree",
+ "SuperDiff::ObjectInspection::InspectionTreeBuilders::Array" => "SuperDiff::Basic::InspectionTreeBuilders::Array",
+ "SuperDiff::ObjectInspection::InspectionTreeBuilders::Base" => "SuperDiff::Core::AbstractInspectionTreeBuilder",
+ "SuperDiff::ObjectInspection::InspectionTreeBuilders::CustomObject" => "SuperDiff::Basic::InspectionTreeBuilders::CustomObject",
+ "SuperDiff::ObjectInspection::InspectionTreeBuilders::DateLike" => "SuperDiff::Basic::InspectionTreeBuilders::DateLike",
+ "SuperDiff::ObjectInspection::InspectionTreeBuilders::DefaultObject" => "SuperDiff::Basic::InspectionTreeBuilders::DefaultObject",
+ "SuperDiff::ObjectInspection::InspectionTreeBuilders::Hash" => "SuperDiff::Basic::InspectionTreeBuilders::Hash",
+ "SuperDiff::ObjectInspection::InspectionTreeBuilders::Primitive" => "SuperDiff::Basic::InspectionTreeBuilders::Primitive",
+ "SuperDiff::ObjectInspection::InspectionTreeBuilders::TimeLike" => "SuperDiff::Basic::InspectionTreeBuilders::TimeLike",
+ "SuperDiff::ObjectInspection::Nodes::AsLinesWhenRenderingToLines" => "SuperDiff::Core::InspectionTreeNodes::AsLinesWhenRenderingToLines",
+ "SuperDiff::ObjectInspection::Nodes::AsPrefixWhenRenderingToLines" => "SuperDiff::Core::InspectionTreeNodes::AsPrefixWhenRenderingToLines",
+ "SuperDiff::ObjectInspection::Nodes::AsPreludeWhenRenderingToLines" => "SuperDiff::Core::InspectionTreeNodes::AsPreludeWhenRenderingToLines",
+ "SuperDiff::ObjectInspection::Nodes::AsSingleLine" => "SuperDiff::Core::InspectionTreeNodes::AsSingleLine",
+ "SuperDiff::ObjectInspection::Nodes::Base" => "SuperDiff::Core::InspectionTreeNodes::Base",
+ "SuperDiff::ObjectInspection::Nodes::Inspection" => "SuperDiff::Core::InspectionTreeNodes::Inspection",
+ "SuperDiff::ObjectInspection::Nodes::Nesting" => "SuperDiff::Core::InspectionTreeNodes::Nesting",
+ "SuperDiff::ObjectInspection::Nodes::OnlyWhen" => "SuperDiff::Core::InspectionTreeNodes::OnlyWhen",
+ "SuperDiff::ObjectInspection::Nodes::Text" => "SuperDiff::Core::InspectionTreeNodes::Text",
+ "SuperDiff::ObjectInspection::Nodes::WhenEmpty" => "SuperDiff::Core::InspectionTreeNodes::WhenEmpty",
+ "SuperDiff::ObjectInspection::Nodes::WhenNonEmpty" => "SuperDiff::Core::InspectionTreeNodes::WhenNonEmpty",
+ "SuperDiff::ObjectInspection::Nodes::WhenRenderingToLines" => "SuperDiff::Core::InspectionTreeNodes::WhenRenderingToLines",
+ "SuperDiff::ObjectInspection::Nodes::WhenRenderingToString" => "SuperDiff::Core::InspectionTreeNodes::WhenRenderingToString",
+ "SuperDiff::ObjectInspection::PrefixForNextNode" => "SuperDiff::Core::PrefixForNextInspectionTreeNode",
+ "SuperDiff::ObjectInspection::PreludeForNextNode" => "SuperDiff::Core::PreludeForNextInspectionTreeNode",
+ "SuperDiff::OperationTreeBuilders::Array" => "SuperDiff::Basic::OperationTreeBuilders::Array",
+ "SuperDiff::OperationTreeBuilders::Base" => "SuperDiff::Core::AbstractOperationTreeBuilder",
+ "SuperDiff::OperationTreeBuilders::CustomObject" => "SuperDiff::Basic::OperationTreeBuilders::CustomObject",
+ "SuperDiff::OperationTreeBuilders::DateLike" => "SuperDiff::Basic::OperationTreeBuilders::DateLike",
+ "SuperDiff::OperationTreeBuilders::DefaultObject" => "SuperDiff::Basic::OperationTreeBuilders::DefaultObject",
+ "SuperDiff::OperationTreeBuilders::Hash" => "SuperDiff::Basic::OperationTreeBuilders::Hash",
+ "SuperDiff::OperationTreeBuilders::MultilineString" => "SuperDiff::Basic::OperationTreeBuilders::MultilineString",
+ "SuperDiff::OperationTreeBuilders::TimeLike" => "SuperDiff::Basic::OperationTreeBuilders::TimeLike",
+ "SuperDiff::OperationTreeFlatteners::Array" => "SuperDiff::Basic::OperationTreeFlatteners::Array",
+ "SuperDiff::OperationTreeFlatteners::Base" => "SuperDiff::Core::AbstractOperationTreeFlattener",
+ "SuperDiff::OperationTreeFlatteners::Collection" => "SuperDiff::Basic::OperationTreeFlatteners::Collection",
+ "SuperDiff::OperationTreeFlatteners::CustomObject" => "SuperDiff::Basic::OperationTreeFlatteners::CustomObject",
+ "SuperDiff::OperationTreeFlatteners::DefaultObject" => "SuperDiff::Basic::OperationTreeFlatteners::DefaultObject",
+ "SuperDiff::OperationTreeFlatteners::Hash" => "SuperDiff::Basic::OperationTreeFlatteners::Hash",
+ "SuperDiff::OperationTreeFlatteners::MultilineString" => "SuperDiff::Basic::OperationTreeFlatteners::MultilineString",
+ "SuperDiff::OperationTrees::Array" => "SuperDiff::Basic::OperationTrees::Array",
+ "SuperDiff::OperationTrees::Base" => "SuperDiff::Core::AbstractOperationTree",
+ "SuperDiff::OperationTrees::CustomObject" => "SuperDiff::Basic::OperationTrees::CustomObject",
+ "SuperDiff::OperationTrees::DefaultObject" => "SuperDiff::Basic::OperationTrees::DefaultObject",
+ "SuperDiff::OperationTrees::Hash" => "SuperDiff::Basic::OperationTrees::Hash",
+ "SuperDiff::OperationTrees::MultilineString" => "SuperDiff::Basic::OperationTrees::MultilineString",
+ "SuperDiff::Operations::BinaryOperation" => "SuperDiff::Core::BinaryOperation",
+ "SuperDiff::Operations::UnaryOperation" => "SuperDiff::Core::UnaryOperation",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::CollectionContainingExactly" => "SuperDiff::RSpec::InspectionTreeBuilders::CollectionContainingExactly",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::CollectionIncluding" => "SuperDiff::RSpec::InspectionTreeBuilders::CollectionIncluding",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::Double" => "SuperDiff::RSpec::InspectionTreeBuilders::Double",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::GenericDescribableMatcher" => "SuperDiff::RSpec::InspectionTreeBuilders::GenericDescribableMatcher",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::HashIncluding" => "SuperDiff::RSpec::InspectionTreeBuilders::HashIncluding",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::InstanceOf" => "SuperDiff::RSpec::InspectionTreeBuilders::InstanceOf",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::KindOf" => "SuperDiff::RSpec::InspectionTreeBuilders::KindOf",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::ObjectHavingAttributes" => "SuperDiff::RSpec::InspectionTreeBuilders::ObjectHavingAttributes",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::Primitive" => "SuperDiff::RSpec::InspectionTreeBuilders::Primitive",
+ "SuperDiff::RSpec::ObjectInspection::InspectionTreeBuilders::ValueWithin" => "SuperDiff::RSpec::InspectionTreeBuilders::ValueWithin",
+ "SuperDiff::RecursionGuard" => "SuperDiff::Core::RecursionGuard",
+ "SuperDiff::TieredLines" => "SuperDiff::Core::TieredLines",
+ "SuperDiff::TieredLinesElider" => "SuperDiff::Core::TieredLinesElider",
+ "SuperDiff::TieredLinesFormatter" => "SuperDiff::Core::TieredLinesFormatter",
+}
+
+# stree-ignore
+active_record_constant_remappings = {
+ "SuperDiff::ActiveRecord::ObjectInspection::InspectionTreeBuilders::ActiveRecordModel" => "SuperDiff::ActiveRecord::InspectionTreeBuilders::ActiveRecordModel",
+ "SuperDiff::ActiveRecord::ObjectInspection::InspectionTreeBuilders::ActiveRecordRelation" => "SuperDiff::ActiveRecord::InspectionTreeBuilders::ActiveRecordRelation",
+}
+
+# stree-ignore
+active_support_constant_remappings = {
+ "SuperDiff::ActiveSupport::ObjectInspection::InspectionTreeBuilders::HashWithIndifferentAccess" => "SuperDiff::ActiveSupport::InspectionTreeBuilders::HashWithIndifferentAccess",
+ "SuperDiff::ActiveSupport::ObjectInspection::InspectionTreeBuilders::OrderedOptions" => "SuperDiff::ActiveSupport::InspectionTreeBuilders::OrderedOptions",
+}
+
+common_constant_remappings.each do |old_constant_name, new_constant_name|
+ RSpec.describe old_constant_name, type: :unit do
+ it "maps to #{new_constant_name}" do
+ capture_warnings do
+ expect(Object.const_get(old_constant_name)).to be(
+ Object.const_get(new_constant_name)
+ )
+ end
+ end
+
+ it "points users to #{new_constant_name} instead" do
+ expect(old_constant_name).to be_deprecated_in_favor_of(new_constant_name)
+ end
+ end
+end
+
+active_record_constant_remappings.each do |old_constant_name, new_constant_name|
+ RSpec.describe old_constant_name, type: :unit, active_record: true do
+ it "maps to #{new_constant_name}" do
+ capture_warnings do
+ expect(Object.const_get(old_constant_name)).to be(
+ Object.const_get(new_constant_name)
+ )
+ end
+ end
+
+ it "points users to #{new_constant_name} instead" do
+ expect(old_constant_name).to be_deprecated_in_favor_of(new_constant_name)
+ end
+ end
+end
+
+active_support_constant_remappings.each do |old_constant_name, new_constant_name|
+ RSpec.describe old_constant_name, type: :unit, active_support: true do
+ it "maps to #{new_constant_name}" do
+ capture_warnings do
+ expect(Object.const_get(old_constant_name)).to be(
+ Object.const_get(new_constant_name)
+ )
+ end
+ end
+
+ it "points users to #{new_constant_name} instead" do
+ expect(old_constant_name).to be_deprecated_in_favor_of(new_constant_name)
+ end
+ end
+end
+
+RSpec.describe "SuperDiff::Differs::Main.call", type: :unit do
+ it "maps to SuperDiff.diff" do
+ capture_warnings do
+ diff_before = SuperDiff::Differs::Main.call(1, 2)
+ diff_after = SuperDiff.diff(1, 2)
+ expect(diff_before).to eq(diff_after)
+ end
+ end
+end
+
+RSpec.describe "SuperDiff::OperationTreeBuilders::Main.call", type: :unit do
+ it "maps to SuperDiff.build_operation_tree_for" do
+ capture_warnings do
+ operation_tree_builder_before =
+ SuperDiff::OperationTreeBuilders::Main.call(1, 2)
+ operation_tree_builder_after = SuperDiff.build_operation_tree_for(1, 2)
+ expect(operation_tree_builder_before).to eq(operation_tree_builder_after)
+ end
+ end
+end
+
+RSpec.describe "SuperDiff::OperationTrees::Main.call", type: :unit do
+ it "maps to SuperDiff.find_operation_tree_for" do
+ capture_warnings do
+ operation_tree_before =
+ SuperDiff::OperationTrees::Main.call(%i[foo bar baz])
+ operation_tree_after = SuperDiff.find_operation_tree_for(%i[foo bar baz])
+ expect(operation_tree_before).to eq(operation_tree_after)
+ end
+ end
+end
diff --git a/spec/unit/equality_matchers/main_spec.rb b/spec/unit/equality_matchers/main_spec.rb
index 51ec3d30..975149e5 100644
--- a/spec/unit/equality_matchers/main_spec.rb
+++ b/spec/unit/equality_matchers/main_spec.rb
@@ -1598,8 +1598,8 @@
#{
colored do
- expected_line %(Expected: #)
- actual_line %( Actual: #)
+ expected_line %(Expected: #)
+ actual_line %( Actual: #)
end
}
@@ -1607,7 +1607,7 @@
#{
colored do
- plain_line %( #)
- actual_line %( Actual: #)
+ expected_line %(Expected: #)
+ actual_line %( Actual: #)
end
}
STR
diff --git a/super_diff.gemspec b/super_diff.gemspec
index 7c80d48a..ef91f11e 100644
--- a/super_diff.gemspec
+++ b/super_diff.gemspec
@@ -13,6 +13,12 @@ Gem::Specification.new do |s|
SuperDiff is a gem that hooks into RSpec to intelligently display the
differences between two data structures of any type.
DESC
+ s.metadata = {
+ "bug_tracker_uri" => "https://github.com/mcmire/super_diff/issues",
+ "changelog_uri" =>
+ "https://github.com/mcmire/super_diff/blob/main/CHANGELOG.md",
+ "source_code_uri" => "https://github.com/mcmire/super_diff"
+ }
s.required_ruby_version = ">= 3"
s.files = %w[README.md super_diff.gemspec] + Dir["lib/**/*"]
diff --git a/tea.yaml b/tea.yaml
new file mode 100644
index 00000000..2506d5ca
--- /dev/null
+++ b/tea.yaml
@@ -0,0 +1,6 @@
+# https://tea.xyz/what-is-this-file
+---
+version: 1.0.0
+codeOwners:
+ - '0xdAfbEDcFc082a2d89293C821E63949Bf13097281'
+quorum: 1
diff --git a/yarn.lock b/yarn.lock
index d5fdf79a..94e574e8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -704,10 +704,10 @@ __metadata:
languageName: node
linkType: hard
-"minipass@npm:^4.0.0":
- version: 4.0.3
- resolution: "minipass@npm:4.0.3"
- checksum: a09f405e2f380ae7f6ee0cbb53b45c1fcc1b6c70fc3896f4d20649d92a10e61892c57bd9960a64cedf6c90b50022cb6c195905b515039c335b423202f99e6f18
+"minipass@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "minipass@npm:5.0.0"
+ checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea
languageName: node
linkType: hard
@@ -1123,16 +1123,16 @@ __metadata:
linkType: hard
"tar@npm:^6.0.2":
- version: 6.1.13
- resolution: "tar@npm:6.1.13"
+ version: 6.2.1
+ resolution: "tar@npm:6.2.1"
dependencies:
chownr: ^2.0.0
fs-minipass: ^2.0.0
- minipass: ^4.0.0
+ minipass: ^5.0.0
minizlib: ^2.1.1
mkdirp: ^1.0.3
yallist: ^4.0.0
- checksum: 8a278bed123aa9f53549b256a36b719e317c8b96fe86a63406f3c62887f78267cea9b22dc6f7007009738509800d4a4dccc444abd71d762287c90f35b002eb1c
+ checksum: f1322768c9741a25356c11373bce918483f40fa9a25c69c59410c8a1247632487edef5fe76c5f12ac51a6356d2f1829e96d2bc34098668a2fc34d76050ac2b6c
languageName: node
linkType: hard