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 - -[![code flow](./docs/code-flow-diagram.png)](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: -![Before super_diff](docs/before.png) +![Before super_diff](docs/assets/before.png) 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: -![After super_diff](docs/after.png) - -## 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: +![After super_diff](docs/assets/after.png) -```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: + +![Before super_diff](../assets/before.png) + +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: + +![After super_diff](../assets/after.png) 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