From d20ed5d0733451138bd617a5a34792b8ce6aba24 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Sat, 10 Feb 2024 16:00:39 -0700 Subject: [PATCH 01/21] Fix extra header in changelog --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 570534ed..9661abf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,8 +122,6 @@ Thank you! ([#91]) -### Features - - Update inspection of Doubles to include stubbed methods and their values. ([#91]) From 1c86d6386908498026da187f58d468415a9bb48f Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Sun, 18 Feb 2024 15:35:27 -0700 Subject: [PATCH 02/21] Tweak GitHub workflows for docsite additions (#224) - Rename `lint` to `analyze`, merge `audit` step into it - Ensure all steps use `ubuntu-latest` - Re-run `analyze` when a PR is reopened - Upgrade `checkout` and `setup-node` to latest versions, removing warnings - Rename `all` to `ready-to-merge` --- .github/workflows/super_diff.yml | 74 ++++++++++---------------------- 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/.github/workflows/super_diff.yml b/.github/workflows/super_diff.yml index 25048ffe..c225488d 100644 --- a/.github/workflows/super_diff.yml +++ b/.github/workflows/super_diff.yml @@ -7,33 +7,29 @@ on: pull_request: types: - opened + - reopened - synchronize concurrency: group: build-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - all: + analyze: 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: - runs-on: ubuntu-latest - 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 +37,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: @@ -97,7 +61,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 +80,11 @@ 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." From 1a5549e9e83fa529fe8ae6fb8d6e226a2e17b08e Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Sun, 18 Feb 2024 17:10:11 -0700 Subject: [PATCH 03/21] Add docsite (#225) `docs/` now holds Markdown files that will be published to a docsite generated by `mkdocs` and hosted on GitHub Pages. Using `mkdocs` allows us to try out literate programming. This requires Python, so that's been added to the `setup` script. Thanks to some new GitHub actions, provided there have been any changes to the docs, a preview version of the docsite will be automatically published when a pull request is updated (in which case the URL will be `https://mcmire.github.io/super_diff/branches//`), and a release version will be published when a release PR is merged (in which case it will be `https://mcmire.github.io/super_diff/releases/`). Any preview version that have been previously been created for a pull request will be deleted when that pull request is closed. The CONTRIBUTING and ARCHITECTURE docs have been moved into `docs/` and expanded to not only explain how SuperDiff works but also the RSpec integration. --- .editorconfig | 2 +- .github/workflows/super_diff.yml | 178 +++++++++++ .gitignore | 6 + .prettierignore | 2 + .python-version | 1 + ARCHITECTURE.md | 33 -- CONTRIBUTING.md | 72 ----- README.md | 186 ++---------- bin/setup | 86 +++++- {docs => docs-support}/after.rb | 0 {docs => docs-support}/before.rb | 0 {docs => docs-support}/carbon-config.json | 0 {docs => docs-support}/carbon.md | 0 {docs => docs-support}/code-flow-diagram.png | Bin docs/{ => assets}/after.png | Bin docs/{ => assets}/before.png | Bin .../architecture/how-rspec-works.md | 224 ++++++++++++++ .../architecture/how-super-diff-works.md | 186 ++++++++++++ .../contributors/architecture/introduction.md | 10 + docs/contributors/how-to-contribute.md | 123 ++++++++ docs/contributors/index.md | 7 + docs/index.md | 14 + docs/users/customization.md | 281 ++++++++++++++++++ docs/users/getting-started.md | 118 ++++++++ docs/users/index.md | 74 +++++ mkdocs.yml | 48 +++ pyproject.toml | 17 ++ scripts/collect-release-info.rb | 19 ++ 28 files changed, 1411 insertions(+), 276 deletions(-) create mode 100644 .python-version delete mode 100644 ARCHITECTURE.md delete mode 100644 CONTRIBUTING.md rename {docs => docs-support}/after.rb (100%) rename {docs => docs-support}/before.rb (100%) rename {docs => docs-support}/carbon-config.json (100%) rename {docs => docs-support}/carbon.md (100%) rename {docs => docs-support}/code-flow-diagram.png (100%) rename docs/{ => assets}/after.png (100%) rename docs/{ => assets}/before.png (100%) create mode 100644 docs/contributors/architecture/how-rspec-works.md create mode 100644 docs/contributors/architecture/how-super-diff-works.md create mode 100644 docs/contributors/architecture/introduction.md create mode 100644 docs/contributors/how-to-contribute.md create mode 100644 docs/contributors/index.md create mode 100644 docs/index.md create mode 100644 docs/users/customization.md create mode 100644 docs/users/getting-started.md create mode 100644 docs/users/index.md create mode 100644 mkdocs.yml create mode 100644 pyproject.toml create mode 100755 scripts/collect-release-info.rb 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 c225488d..f4339ee5 100644 --- a/.github/workflows/super_diff.yml +++ b/.github/workflows/super_diff.yml @@ -7,6 +7,7 @@ on: pull_request: types: - opened + - closed - reopened - synchronize concurrency: @@ -15,6 +16,7 @@ concurrency: jobs: analyze: runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' || github.event.action != 'closed' }} steps: - uses: actions/checkout@v4 - name: Download actionlint @@ -88,3 +90,179 @@ jobs: - 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 [[ "$IS_NEW_RELEASE" == "true" ]]; then + DOCSITE_RELEASE_VERSION="$RELEASE_VERSION" + DOCSITE_DESTINATION_PATH="releases/$RELEASE_VERSION" + HAS_CHANGES_TO_DOCS="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_CHANGES_TO_DOCS="false" + else + HAS_CHANGES_TO_DOCS="true" + fi + fi + + { + echo "DOCSITE_RELEASE_VERSION=$DOCSITE_RELEASE_VERSION" + echo "DOCSITE_DESTINATION_PATH=$DOCSITE_DESTINATION_PATH" + echo "HAS_CHANGES_TO_DOCS=$HAS_CHANGES_TO_DOCS" + } >> "$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_CHANGES_TO_DOCS: ${{ steps.command.outputs.HAS_CHANGES_TO_DOCS }} + + build-docsite: + runs-on: ubuntu-latest + needs: + - analyze + - collect-release-info + - collect-docsite-release-info + if: ${{ github.event_name == 'pull_request' && ((needs.collect-release-info.outputs.IS_NEW_RELEASE == 'false' && needs.collect-docsite-release-info.outputs.HAS_CHANGES_TO_DOCS == 'true') || (needs.collect-release-info.outputs.IS_NEW_RELEASE == 'true' && github.event.merged)) }} + 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 + - 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: | + cat <<-EOT > index.html + + + + SuperDiff Documentation + + + +

+ This page has moved to a different URL. + Please click + + this link + + if you are not redirected. +

+ + + EOT + - 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 + 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/.prettierignore b/.prettierignore index 8fe001fd..22e00978 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,7 @@ .yarn/cache .yarn/releases gemfiles +site +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/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..9bc7347d 100644 --- a/README.md +++ b/README.md @@ -8,24 +8,26 @@ [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`, +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 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 +35,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 +79,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 +88,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: +![After super_diff](docs/assets/after.png) -```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" -``` +## Installation & Usage -## Configuration +📘 For more on how to install and use SuperDiff, +[read the user documentation][user-docs]. -You can customize the behavior of the gem -by adding a configuration block -to your test helper file -(`rails_helper` or `spec_helper`) -which looks something like this: - -```ruby -SuperDiff.configure do |config| - # ... -end -``` - -### Customizing colors - -If you don't like the colors that SuperDiff uses, -you can change them like this: - -```ruby -SuperDiff.configure do |config| - config.actual_color = :green - config.expected_color = :red - config.border_color = :yellow - config.header_color = :yellow -end -``` - -See [eight_bit_color.rb](lib/super_diff/csi/eight_bit_color.rb) -for the list of available colors. - -You can also completely disable colorized output. - - -```ruby -SuperDiff.configure do |config| - config.color_enabled = false - end -``` - - -### Disabling the key - -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 +110,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..e2c7240b --- /dev/null +++ b/docs/contributors/architecture/how-rspec-works.md @@ -0,0 +1,224 @@ +# 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..6a86ba85 --- /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** (or, casually, an _inspector_) + makes use of an **inspection tree** + to generate 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 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_, or _change_. + 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 inspectors) +- `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::Differs::Main.call` is called with a pair of values: `expected` and `actual`. + This method looks for a differ that is suitable for the pair + among a set of defaults and the list of differs registered via SuperDiff's configuration. + It does this by calling `.applies_to?` on each, + 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::Differs::Base`, + `.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::Differs::Array` uses a `SuperDiff::OperationTreeBuilder::Array`, + `SuperDiff::Differs::Hash` uses a `SuperDiff::OperationTreeBuilder::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::OperationTrees::Base`, + 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 a formatter — + so called `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 + DiffersMain["Differs::Main"] -- 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[Final diff]; +``` diff --git a/docs/contributors/architecture/introduction.md b/docs/contributors/architecture/introduction.md new file mode 100644 index 00000000..3ca96d41 --- /dev/null +++ b/docs/contributors/architecture/introduction.md @@ -0,0 +1,10 @@ +# 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). diff --git a/docs/contributors/how-to-contribute.md b/docs/contributors/how-to-contribute.md new file mode 100644 index 00000000..e0c1c86f --- /dev/null +++ b/docs/contributors/how-to-contribute.md @@ -0,0 +1,123 @@ +# 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. + +[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/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/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..e2fb439d --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,48 @@ +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" + +#plugins: +#- entangled # this also runs `entangled sync` as a pre-build action + +markdown_extensions: + - admonition + - 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/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 From 638eb78e6c24a150d4f13111857c0cb5f22517c3 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Sun, 18 Feb 2024 20:21:09 -0700 Subject: [PATCH 04/21] Fix GitHub workflow (#226) This ends up creating a rogue `GITHUB_ENV` file on the `gh-pages` branch instead of setting an environment variable. --- .github/workflows/super_diff.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/super_diff.yml b/.github/workflows/super_diff.yml index f4339ee5..049c588d 100644 --- a/.github/workflows/super_diff.yml +++ b/.github/workflows/super_diff.yml @@ -246,7 +246,7 @@ jobs: run: | set -x DOCSITE_DESTINATION_PARENT_PATH="$(dirname "$DOCSITE_DESTINATION_PATH")" - echo "DOCSITE_DESTINATION_PARENT_PATH=$DOCSITE_DESTINATION_PARENT_PATH" >> "GITHUB_ENV" + 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 From d279f80e73ead42f53933b95a9402a5e6e265085 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Thu, 22 Feb 2024 20:04:34 -0700 Subject: [PATCH 05/21] Use Node LTS for development (#227) --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index e6db45a9..b009dfb9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.14.0 +lts/* From 0d764058e3009b873196efad1a685a9f27ac99a7 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Thu, 22 Feb 2024 20:02:10 -0700 Subject: [PATCH 06/21] Remove duplicate copy from README --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 9bc7347d..8c2843eb 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,6 @@ in a familiar and intelligent fashion. ## Introduction -The primary motivation behind this gem -is to vastly improve upon RSpec's built-in diffing capabilities. - The primary motivation behind this gem is to vastly improve upon RSpec's built-in diffing capabilities. RSpec has many nice features, From 4625998f7ddb664614ab5629c58a4b4ebce9cd6b Mon Sep 17 00:00:00 2001 From: y-yagi Date: Sun, 25 Feb 2024 06:07:51 +0900 Subject: [PATCH 07/21] CI against Ruby 3.3 (#229) This PR adds Ruby 3.3 to the CI matrix to confirm this gem works with Ruby 3.3. Co-authored-by: Elliot Winkler --- .github/workflows/super_diff.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/super_diff.yml b/.github/workflows/super_diff.yml index 049c588d..cf648fb9 100644 --- a/.github/workflows/super_diff.yml +++ b/.github/workflows/super_diff.yml @@ -53,6 +53,7 @@ jobs: - "3.0" - "3.1" - "3.2" + - "3.3" rails_appraisal: - rails_6_1 - rails_7_0 From fc418c15f04925e00f32298fc85f74c57591fc69 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 1 Mar 2024 00:08:41 -0700 Subject: [PATCH 08/21] Reorganize codebase (#230) In attempting to detail the architecture of this codebase, I realized that it would be easier if the parts of SuperDiff that implemented differs, inspection tree builders, operation tree builders, operation tree flatteners, and operation trees for Ruby objects were split off into their own directory. To that end, alongside RSpec, ActiveRecord, and ActiveSupport, there is now a "Basic" feature module. In addition, to make the file structure a little flatter, I've also renamed `InspectionTreeBuilders` in various feature modules up one level (so, `ObjectInspection::InspectionTreeBuilders` is now simply `InspectionTreeBuilders`). Following this change, SuperDiff can now be divided into 4 layers (here I'm putting CSI in brackets because it's not a public module): ``` ----------------- [SuperDiff::CSI] / / | / SuperDiff::Core ---------------------- | / | \ | \ | /^^^^^^ | SuperDiff::Basic \ | / | / | \ \ | | /^^^^^^^^^^^|^^^^ | ^^^^^^^^^^^^^\ \ SuperDiff::RSpec SuperDiff::ActiveRecord SuperDiff::ActiveSupport ``` Here is a full 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` To maintain backward compatibility, I've left all of the original constants in place, but they will now print deprecation warnings when used. I will remove them in 1.0.0. --- lib/super_diff.rb | 90 +++- lib/super_diff/active_record.rb | 28 +- .../differs/active_record_relation.rb | 2 +- .../active_record/inspection_tree_builders.rb | 14 + .../active_record_model.rb | 49 ++ .../active_record_relation.rb | 34 ++ .../active_record/monkey_patches.rb | 1 + .../active_record/object_inspection.rb | 20 +- .../inspection_tree_builders.rb | 16 - .../active_record_model.rb | 51 -- .../active_record_relation.rb | 36 -- .../active_record_model.rb | 2 +- .../active_record_relation.rb | 2 +- .../active_record_relation.rb | 2 +- .../operation_trees/active_record_relation.rb | 2 +- lib/super_diff/active_support.rb | 26 +- .../differs/hash_with_indifferent_access.rb | 2 +- .../inspection_tree_builders.rb | 14 + .../hash_with_indifferent_access.rb | 44 ++ .../ordered_options.rb | 44 ++ .../active_support/object_inspection.rb | 20 +- .../inspection_tree_builders.rb | 16 - .../hash_with_indifferent_access.rb | 46 -- .../ordered_options.rb | 46 -- .../hash_with_indifferent_access.rb | 2 +- .../hash_with_indifferent_access.rb | 2 +- .../hash_with_indifferent_access.rb | 2 +- lib/super_diff/basic.rb | 48 ++ lib/super_diff/basic/diff_formatters.rb | 11 + .../basic/diff_formatters/collection.rb | 135 +++++ .../basic/diff_formatters/multiline_string.rb | 34 ++ lib/super_diff/basic/differs.rb | 24 + lib/super_diff/basic/differs/array.rb | 17 + lib/super_diff/basic/differs/custom_object.rb | 19 + lib/super_diff/basic/differs/date_like.rb | 17 + .../basic/differs/default_object.rb | 24 + lib/super_diff/basic/differs/hash.rb | 17 + .../basic/differs/multiline_string.rb | 18 + lib/super_diff/basic/differs/time_like.rb | 17 + .../basic/inspection_tree_builders.rb | 20 + .../inspection_tree_builders/array.rb | 6 +- .../inspection_tree_builders/custom_object.rb | 6 +- .../inspection_tree_builders/date_like.rb | 6 +- .../default_object.rb | 12 +- .../inspection_tree_builders/hash.rb | 6 +- .../inspection_tree_builders/primitive.rb | 6 +- .../inspection_tree_builders/time_like.rb | 6 +- .../basic/operation_tree_builders.rb | 34 ++ .../basic/operation_tree_builders/array.rb | 111 ++++ .../operation_tree_builders/custom_object.rb | 42 ++ .../operation_tree_builders/date_like.rb | 17 + .../operation_tree_builders/default_object.rb | 117 +++++ .../basic/operation_tree_builders/hash.rb | 222 ++++++++ .../multiline_string.rb | 90 ++++ .../operation_tree_builders/time_like.rb | 26 + .../basic/operation_tree_flatteners.rb | 24 + .../basic/operation_tree_flatteners/array.rb | 17 + .../operation_tree_flatteners/collection.rb | 140 ++++++ .../custom_object.rb | 30 ++ .../default_object.rb | 32 ++ .../basic/operation_tree_flatteners/hash.rb | 35 ++ .../multiline_string.rb | 20 + lib/super_diff/basic/operation_trees.rb | 25 + lib/super_diff/basic/operation_trees/array.rb | 17 + .../basic/operation_trees/custom_object.rb | 17 + .../basic/operation_trees/default_object.rb | 42 ++ lib/super_diff/basic/operation_trees/hash.rb | 17 + .../basic/operation_trees/multiline_string.rb | 17 + .../colorized_document_extensions.rb | 18 - lib/super_diff/configuration.rb | 149 ------ lib/super_diff/core.rb | 69 +++ .../base.rb => core/abstract_differ.rb} | 4 +- .../core/abstract_inspection_tree_builder.rb | 26 + .../abstract_operation_tree.rb} | 8 +- .../abstract_operation_tree_builder.rb} | 12 +- .../abstract_operation_tree_flattener.rb} | 4 +- .../{operations => core}/binary_operation.rb | 2 +- .../core/colorized_document_extensions.rb | 20 + lib/super_diff/core/configuration.rb | 192 +++++++ lib/super_diff/core/differ_dispatcher.rb | 33 ++ lib/super_diff/core/gem_version.rb | 47 ++ lib/super_diff/core/helpers.rb | 88 ++++ lib/super_diff/core/implementation_checks.rb | 21 + .../inspection_tree.rb | 13 +- .../inspection_tree_builder_dispatcher.rb | 23 + lib/super_diff/core/inspection_tree_nodes.rb | 55 ++ .../as_lines_when_rendering_to_lines.rb | 12 +- .../as_prefix_when_rendering_to_lines.rb | 6 +- .../as_prelude_when_rendering_to_lines.rb | 6 +- .../inspection_tree_nodes}/as_single_line.rb | 6 +- .../inspection_tree_nodes}/base.rb | 4 +- .../inspection_tree_nodes}/inspection.rb | 18 +- .../inspection_tree_nodes}/nesting.rb | 4 +- .../inspection_tree_nodes}/only_when.rb | 4 +- .../inspection_tree_nodes}/text.rb | 4 +- .../inspection_tree_nodes}/when_empty.rb | 4 +- .../inspection_tree_nodes}/when_non_empty.rb | 4 +- .../when_rendering_to_lines.rb | 4 +- .../when_rendering_to_string.rb | 4 +- lib/super_diff/core/line.rb | 85 ++++ .../no_differ_available_error.rb | 2 +- ...inspection_tree_builder_available_error.rb | 21 + .../core/no_operation_tree_available_error.rb | 20 + ..._operation_tree_builder_available_error.rb | 24 + .../operation_tree_builder_dispatcher.rb} | 22 +- lib/super_diff/core/operation_tree_finder.rb | 27 + .../prefix_for_next_inspection_tree_node.rb | 6 + .../prelude_for_next_inspection_tree_node.rb | 6 + lib/super_diff/core/recursion_guard.rb | 52 ++ lib/super_diff/core/tiered_lines.rb | 6 + lib/super_diff/core/tiered_lines_elider.rb | 472 ++++++++++++++++++ lib/super_diff/core/tiered_lines_formatter.rb | 77 +++ .../{operations => core}/unary_operation.rb | 2 +- lib/super_diff/diff_formatters/collection.rb | 132 ----- .../diff_formatters/multiline_string.rb | 31 -- lib/super_diff/differs.rb | 31 +- lib/super_diff/differs/array.rb | 15 - lib/super_diff/differs/custom_object.rb | 17 - lib/super_diff/differs/date_like.rb | 15 - lib/super_diff/differs/default_object.rb | 19 - lib/super_diff/differs/defaults.rb | 13 - lib/super_diff/differs/empty.rb | 13 - lib/super_diff/differs/hash.rb | 15 - lib/super_diff/differs/main.rb | 31 -- lib/super_diff/differs/multiline_string.rb | 16 - lib/super_diff/differs/time_like.rb | 15 - lib/super_diff/equality_matchers/array.rb | 6 +- lib/super_diff/equality_matchers/default.rb | 11 +- lib/super_diff/equality_matchers/hash.rb | 6 +- .../equality_matchers/multiline_string.rb | 6 +- lib/super_diff/equality_matchers/primitive.rb | 6 +- .../equality_matchers/singleline_string.rb | 4 +- lib/super_diff/errors.rb | 20 +- lib/super_diff/gem_version.rb | 45 -- lib/super_diff/helpers.rb | 86 ---- lib/super_diff/implementation_checks.rb | 19 - lib/super_diff/line.rb | 83 --- lib/super_diff/object_inspection.rb | 77 ++- .../inspection_tree_builders.rb | 48 -- .../inspection_tree_builders/base.rb | 27 - .../inspection_tree_builders/defaults.rb | 15 - .../inspection_tree_builders/main.rb | 30 -- lib/super_diff/object_inspection/nodes.rb | 50 -- .../object_inspection/prefix_for_next_node.rb | 6 - .../prelude_for_next_node.rb | 6 - lib/super_diff/operation_tree_builders.rb | 34 +- .../operation_tree_builders/array.rb | 107 ---- .../operation_tree_builders/custom_object.rb | 40 -- .../operation_tree_builders/date_like.rb | 15 - .../operation_tree_builders/default_object.rb | 119 ----- .../operation_tree_builders/defaults.rb | 5 - .../operation_tree_builders/hash.rb | 218 -------- .../multiline_string.rb | 86 ---- .../operation_tree_builders/time_like.rb | 24 - lib/super_diff/operation_tree_flatteners.rb | 35 +- .../operation_tree_flatteners/array.rb | 15 - .../operation_tree_flatteners/collection.rb | 136 ----- .../custom_object.rb | 28 -- .../default_object.rb | 31 -- .../operation_tree_flatteners/hash.rb | 33 -- .../multiline_string.rb | 18 - lib/super_diff/operation_trees.rb | 28 +- lib/super_diff/operation_trees/array.rb | 15 - .../operation_trees/custom_object.rb | 15 - .../operation_trees/default_object.rb | 40 -- lib/super_diff/operation_trees/defaults.rb | 5 - lib/super_diff/operation_trees/hash.rb | 15 - lib/super_diff/operation_trees/main.rb | 35 -- .../operation_trees/multiline_string.rb | 15 - lib/super_diff/operations.rb | 14 +- lib/super_diff/recursion_guard.rb | 50 -- lib/super_diff/rspec.rb | 38 +- lib/super_diff/rspec/augmented_matcher.rb | 2 +- lib/super_diff/rspec/differ.rb | 9 +- .../differs/collection_containing_exactly.rb | 2 +- .../rspec/differs/collection_including.rb | 2 +- .../rspec/differs/hash_including.rb | 2 +- .../rspec/differs/object_having_attributes.rb | 2 +- .../rspec/inspection_tree_builders.rb | 40 ++ .../collection_containing_exactly.rb | 34 ++ .../collection_including.rb | 40 ++ .../rspec/inspection_tree_builders/double.rb | 100 ++++ .../generic_describable_matcher.rb | 17 + .../hash_including.rb | 40 ++ .../inspection_tree_builders/instance_of.rb | 25 + .../rspec/inspection_tree_builders/kind_of.rb | 25 + .../object_having_attributes.rb | 34 ++ .../inspection_tree_builders/primitive.rb | 9 + .../inspection_tree_builders/value_within.rb | 30 ++ lib/super_diff/rspec/object_inspection.rb | 18 +- .../inspection_tree_builders.rb | 48 -- .../collection_containing_exactly.rb | 36 -- .../collection_including.rb | 42 -- .../inspection_tree_builders/double.rb | 102 ---- .../generic_describable_matcher.rb | 19 - .../hash_including.rb | 42 -- .../inspection_tree_builders/instance_of.rb | 27 - .../inspection_tree_builders/kind_of.rb | 27 - .../object_having_attributes.rb | 36 -- .../inspection_tree_builders/primitive.rb | 10 - .../inspection_tree_builders/value_within.rb | 32 -- .../collection_containing_exactly.rb | 8 +- .../collection_including.rb | 2 +- .../operation_tree_builders/hash_including.rb | 2 +- .../object_having_attributes.rb | 4 +- lib/super_diff/tiered_lines.rb | 4 - lib/super_diff/tiered_lines_elider.rb | 462 ----------------- lib/super_diff/tiered_lines_formatter.rb | 75 --- spec/support/integration/helpers.rb | 5 +- spec/support/unit/helpers.rb | 13 +- .../matchers/be_deprecated_in_favor_of.rb | 39 ++ .../operation_tree_flatteners/array_spec.rb | 16 +- .../custom_object_spec.rb | 18 +- .../default_object_spec.rb | 18 +- .../operation_tree_flatteners/hash_spec.rb | 16 +- .../multiline_string_spec.rb | 8 +- spec/unit/{ => core}/helpers_spec.rb | 4 +- .../{ => core}/tiered_lines_elider_spec.rb | 4 +- .../{ => core}/tiered_lines_formatter_spec.rb | 2 +- spec/unit/deprecations_spec.rb | 176 +++++++ spec/unit/equality_matchers/main_spec.rb | 10 +- 221 files changed, 4202 insertions(+), 3504 deletions(-) create mode 100644 lib/super_diff/active_record/inspection_tree_builders.rb create mode 100644 lib/super_diff/active_record/inspection_tree_builders/active_record_model.rb create mode 100644 lib/super_diff/active_record/inspection_tree_builders/active_record_relation.rb delete mode 100644 lib/super_diff/active_record/object_inspection/inspection_tree_builders.rb delete mode 100644 lib/super_diff/active_record/object_inspection/inspection_tree_builders/active_record_model.rb delete mode 100644 lib/super_diff/active_record/object_inspection/inspection_tree_builders/active_record_relation.rb create mode 100644 lib/super_diff/active_support/inspection_tree_builders.rb create mode 100644 lib/super_diff/active_support/inspection_tree_builders/hash_with_indifferent_access.rb create mode 100644 lib/super_diff/active_support/inspection_tree_builders/ordered_options.rb delete mode 100644 lib/super_diff/active_support/object_inspection/inspection_tree_builders.rb delete mode 100644 lib/super_diff/active_support/object_inspection/inspection_tree_builders/hash_with_indifferent_access.rb delete mode 100644 lib/super_diff/active_support/object_inspection/inspection_tree_builders/ordered_options.rb create mode 100644 lib/super_diff/basic.rb create mode 100644 lib/super_diff/basic/diff_formatters.rb create mode 100644 lib/super_diff/basic/diff_formatters/collection.rb create mode 100644 lib/super_diff/basic/diff_formatters/multiline_string.rb create mode 100644 lib/super_diff/basic/differs.rb create mode 100644 lib/super_diff/basic/differs/array.rb create mode 100644 lib/super_diff/basic/differs/custom_object.rb create mode 100644 lib/super_diff/basic/differs/date_like.rb create mode 100644 lib/super_diff/basic/differs/default_object.rb create mode 100644 lib/super_diff/basic/differs/hash.rb create mode 100644 lib/super_diff/basic/differs/multiline_string.rb create mode 100644 lib/super_diff/basic/differs/time_like.rb create mode 100644 lib/super_diff/basic/inspection_tree_builders.rb rename lib/super_diff/{object_inspection => basic}/inspection_tree_builders/array.rb (90%) rename lib/super_diff/{object_inspection => basic}/inspection_tree_builders/custom_object.rb (88%) rename lib/super_diff/{object_inspection => basic}/inspection_tree_builders/date_like.rb (91%) rename lib/super_diff/{object_inspection => basic}/inspection_tree_builders/default_object.rb (84%) rename lib/super_diff/{object_inspection => basic}/inspection_tree_builders/hash.rb (92%) rename lib/super_diff/{object_inspection => basic}/inspection_tree_builders/primitive.rb (74%) rename lib/super_diff/{object_inspection => basic}/inspection_tree_builders/time_like.rb (93%) create mode 100644 lib/super_diff/basic/operation_tree_builders.rb create mode 100644 lib/super_diff/basic/operation_tree_builders/array.rb create mode 100644 lib/super_diff/basic/operation_tree_builders/custom_object.rb create mode 100644 lib/super_diff/basic/operation_tree_builders/date_like.rb create mode 100644 lib/super_diff/basic/operation_tree_builders/default_object.rb create mode 100644 lib/super_diff/basic/operation_tree_builders/hash.rb create mode 100644 lib/super_diff/basic/operation_tree_builders/multiline_string.rb create mode 100644 lib/super_diff/basic/operation_tree_builders/time_like.rb create mode 100644 lib/super_diff/basic/operation_tree_flatteners.rb create mode 100644 lib/super_diff/basic/operation_tree_flatteners/array.rb create mode 100644 lib/super_diff/basic/operation_tree_flatteners/collection.rb create mode 100644 lib/super_diff/basic/operation_tree_flatteners/custom_object.rb create mode 100644 lib/super_diff/basic/operation_tree_flatteners/default_object.rb create mode 100644 lib/super_diff/basic/operation_tree_flatteners/hash.rb create mode 100644 lib/super_diff/basic/operation_tree_flatteners/multiline_string.rb create mode 100644 lib/super_diff/basic/operation_trees.rb create mode 100644 lib/super_diff/basic/operation_trees/array.rb create mode 100644 lib/super_diff/basic/operation_trees/custom_object.rb create mode 100644 lib/super_diff/basic/operation_trees/default_object.rb create mode 100644 lib/super_diff/basic/operation_trees/hash.rb create mode 100644 lib/super_diff/basic/operation_trees/multiline_string.rb delete mode 100644 lib/super_diff/colorized_document_extensions.rb delete mode 100644 lib/super_diff/configuration.rb create mode 100644 lib/super_diff/core.rb rename lib/super_diff/{differs/base.rb => core/abstract_differ.rb} (93%) create mode 100644 lib/super_diff/core/abstract_inspection_tree_builder.rb rename lib/super_diff/{operation_trees/base.rb => core/abstract_operation_tree.rb} (89%) rename lib/super_diff/{operation_tree_builders/base.rb => core/abstract_operation_tree_builder.rb} (91%) rename lib/super_diff/{operation_tree_flatteners/base.rb => core/abstract_operation_tree_flattener.rb} (94%) rename lib/super_diff/{operations => core}/binary_operation.rb (95%) create mode 100644 lib/super_diff/core/colorized_document_extensions.rb create mode 100644 lib/super_diff/core/configuration.rb create mode 100644 lib/super_diff/core/differ_dispatcher.rb create mode 100644 lib/super_diff/core/gem_version.rb create mode 100644 lib/super_diff/core/helpers.rb create mode 100644 lib/super_diff/core/implementation_checks.rb rename lib/super_diff/{object_inspection => core}/inspection_tree.rb (94%) create mode 100644 lib/super_diff/core/inspection_tree_builder_dispatcher.rb create mode 100644 lib/super_diff/core/inspection_tree_nodes.rb rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/as_lines_when_rendering_to_lines.rb (90%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/as_prefix_when_rendering_to_lines.rb (78%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/as_prelude_when_rendering_to_lines.rb (78%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/as_single_line.rb (88%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/base.rb (98%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/inspection.rb (71%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/nesting.rb (91%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/only_when.rb (95%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/text.rb (92%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/when_empty.rb (94%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/when_non_empty.rb (94%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/when_rendering_to_lines.rb (91%) rename lib/super_diff/{object_inspection/nodes => core/inspection_tree_nodes}/when_rendering_to_string.rb (90%) create mode 100644 lib/super_diff/core/line.rb rename lib/super_diff/{errors => core}/no_differ_available_error.rb (97%) create mode 100644 lib/super_diff/core/no_inspection_tree_builder_available_error.rb create mode 100644 lib/super_diff/core/no_operation_tree_available_error.rb create mode 100644 lib/super_diff/core/no_operation_tree_builder_available_error.rb rename lib/super_diff/{operation_tree_builders/main.rb => core/operation_tree_builder_dispatcher.rb} (53%) create mode 100644 lib/super_diff/core/operation_tree_finder.rb create mode 100644 lib/super_diff/core/prefix_for_next_inspection_tree_node.rb create mode 100644 lib/super_diff/core/prelude_for_next_inspection_tree_node.rb create mode 100644 lib/super_diff/core/recursion_guard.rb create mode 100644 lib/super_diff/core/tiered_lines.rb create mode 100644 lib/super_diff/core/tiered_lines_elider.rb create mode 100644 lib/super_diff/core/tiered_lines_formatter.rb rename lib/super_diff/{operations => core}/unary_operation.rb (88%) delete mode 100644 lib/super_diff/diff_formatters/collection.rb delete mode 100644 lib/super_diff/diff_formatters/multiline_string.rb delete mode 100644 lib/super_diff/differs/array.rb delete mode 100644 lib/super_diff/differs/custom_object.rb delete mode 100644 lib/super_diff/differs/date_like.rb delete mode 100644 lib/super_diff/differs/default_object.rb delete mode 100644 lib/super_diff/differs/defaults.rb delete mode 100644 lib/super_diff/differs/empty.rb delete mode 100644 lib/super_diff/differs/hash.rb delete mode 100644 lib/super_diff/differs/main.rb delete mode 100644 lib/super_diff/differs/multiline_string.rb delete mode 100644 lib/super_diff/differs/time_like.rb delete mode 100644 lib/super_diff/gem_version.rb delete mode 100644 lib/super_diff/helpers.rb delete mode 100644 lib/super_diff/implementation_checks.rb delete mode 100644 lib/super_diff/line.rb delete mode 100644 lib/super_diff/object_inspection/inspection_tree_builders.rb delete mode 100644 lib/super_diff/object_inspection/inspection_tree_builders/base.rb delete mode 100644 lib/super_diff/object_inspection/inspection_tree_builders/defaults.rb delete mode 100644 lib/super_diff/object_inspection/inspection_tree_builders/main.rb delete mode 100644 lib/super_diff/object_inspection/nodes.rb delete mode 100644 lib/super_diff/object_inspection/prefix_for_next_node.rb delete mode 100644 lib/super_diff/object_inspection/prelude_for_next_node.rb delete mode 100644 lib/super_diff/operation_tree_builders/array.rb delete mode 100644 lib/super_diff/operation_tree_builders/custom_object.rb delete mode 100644 lib/super_diff/operation_tree_builders/date_like.rb delete mode 100644 lib/super_diff/operation_tree_builders/default_object.rb delete mode 100644 lib/super_diff/operation_tree_builders/defaults.rb delete mode 100644 lib/super_diff/operation_tree_builders/hash.rb delete mode 100644 lib/super_diff/operation_tree_builders/multiline_string.rb delete mode 100644 lib/super_diff/operation_tree_builders/time_like.rb delete mode 100644 lib/super_diff/operation_tree_flatteners/array.rb delete mode 100644 lib/super_diff/operation_tree_flatteners/collection.rb delete mode 100644 lib/super_diff/operation_tree_flatteners/custom_object.rb delete mode 100644 lib/super_diff/operation_tree_flatteners/default_object.rb delete mode 100644 lib/super_diff/operation_tree_flatteners/hash.rb delete mode 100644 lib/super_diff/operation_tree_flatteners/multiline_string.rb delete mode 100644 lib/super_diff/operation_trees/array.rb delete mode 100644 lib/super_diff/operation_trees/custom_object.rb delete mode 100644 lib/super_diff/operation_trees/default_object.rb delete mode 100644 lib/super_diff/operation_trees/defaults.rb delete mode 100644 lib/super_diff/operation_trees/hash.rb delete mode 100644 lib/super_diff/operation_trees/main.rb delete mode 100644 lib/super_diff/operation_trees/multiline_string.rb delete mode 100644 lib/super_diff/recursion_guard.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/collection_containing_exactly.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/collection_including.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/double.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/generic_describable_matcher.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/hash_including.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/instance_of.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/kind_of.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/object_having_attributes.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/primitive.rb create mode 100644 lib/super_diff/rspec/inspection_tree_builders/value_within.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/collection_containing_exactly.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/collection_including.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/double.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/generic_describable_matcher.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/hash_including.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/instance_of.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/kind_of.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/object_having_attributes.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/primitive.rb delete mode 100644 lib/super_diff/rspec/object_inspection/inspection_tree_builders/value_within.rb delete mode 100644 lib/super_diff/tiered_lines.rb delete mode 100644 lib/super_diff/tiered_lines_elider.rb delete mode 100644 lib/super_diff/tiered_lines_formatter.rb create mode 100644 spec/support/unit/matchers/be_deprecated_in_favor_of.rb rename spec/unit/{ => basic}/operation_tree_flatteners/array_spec.rb (97%) rename spec/unit/{ => basic}/operation_tree_flatteners/custom_object_spec.rb (97%) rename spec/unit/{ => basic}/operation_tree_flatteners/default_object_spec.rb (97%) rename spec/unit/{ => basic}/operation_tree_flatteners/hash_spec.rb (97%) rename spec/unit/{ => basic}/operation_tree_flatteners/multiline_string_spec.rb (92%) rename spec/unit/{ => core}/helpers_spec.rb (94%) rename spec/unit/{ => core}/tiered_lines_elider_spec.rb (99%) rename spec/unit/{ => core}/tiered_lines_formatter_spec.rb (98%) create mode 100644 spec/unit/deprecations_spec.rb 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..25bf2a2c --- /dev/null +++ b/lib/super_diff/active_record/inspection_tree_builders/active_record_model.rb @@ -0,0 +1,49 @@ +module SuperDiff + module ActiveRecord + module InspectionTreeBuilders + class ActiveRecordModel < Core::AbstractInspectionTreeBuilder + def self.applies_to?(value) + value.is_a?(::ActiveRecord::Base) + 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 + + 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 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..b2ebce7e 100644 --- a/lib/super_diff/active_record/monkey_patches.rb +++ b/lib/super_diff/active_record/monkey_patches.rb @@ -1,5 +1,6 @@ # 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]) 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..72442b08 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 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..5321ff49 --- /dev/null +++ b/lib/super_diff/core/differ_dispatcher.rb @@ -0,0 +1,33 @@ +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 + pp available_classes: available_classes + + 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/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/unit/helpers.rb b/spec/support/unit/helpers.rb index 5b1901d1..580dcea0 100644 --- a/spec/support/unit/helpers.rb +++ b/spec/support/unit/helpers.rb @@ -1,5 +1,7 @@ module SuperDiff module UnitTests + extend self + def with_configuration(configuration) old_configuration = SuperDiff.configuration.dup SuperDiff.configuration.merge!(configuration) @@ -7,7 +9,16 @@ def with_configuration(configuration) end def colored(*args, **opts, &block) - SuperDiff::Helpers.style(*args, **opts, &block).to_s.chomp + SuperDiff::Core::Helpers.style(*args, **opts, &block).to_s.chomp + end + + def capture_warnings + fake_stderr = StringIO.new + original_stderr = $stderr + $stderr = fake_stderr + yield + $stderr = original_stderr + fake_stderr.string end end end diff --git a/spec/support/unit/matchers/be_deprecated_in_favor_of.rb b/spec/support/unit/matchers/be_deprecated_in_favor_of.rb new file mode 100644 index 00000000..8d5773bb --- /dev/null +++ b/spec/support/unit/matchers/be_deprecated_in_favor_of.rb @@ -0,0 +1,39 @@ +module SuperDiff + module UnitTests + def be_deprecated_in_favor_of(new_constant_name) + BeDeprecatedInFavorOfMatcher.new(new_constant_name) + end + + class BeDeprecatedInFavorOfMatcher + extend AttrExtras.mixin + + pattr_initialize :new_constant_name + attr_private :actual_output, :old_constant_name + + def matches?(old_constant_name) + @old_constant_name = old_constant_name + @actual_output = + SuperDiff::UnitTests.capture_warnings do + Object.const_get(old_constant_name) + end + @actual_output.start_with?(expected_prefix) + end + + def failure_message + "Expected stderr to start with:\n\n" + + SuperDiff::Test::OutputHelpers.bookended(expected_prefix) + "\n" + + "Actual output:\n\n" + + SuperDiff::Test::OutputHelpers.bookended(actual_output) + end + + private + + def expected_prefix + <<~EOT.rstrip + WARNING: #{old_constant_name} is deprecated and will be removed in the next major release. + Please use #{new_constant_name} instead. + EOT + end + end + end +end 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 From de1f3137f1c834435d683ade11df66575da66251 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 1 Mar 2024 20:28:01 -0700 Subject: [PATCH 09/21] Update changelog for recent InspectionTree change (#231) Recently, InspectionTree was changed so that it no longer `instance_eval`'s the block that it takes. This change was released in 0.11.0, but should have been marked as a breaking change, since it affects custom InspectionTreeBuilders. Although we can't go back and change 0.11.0, we can at least document this for future code spelunkers. --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9661abf6..189f0ee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,45 @@ ## 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 +64,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)) From a13efa5aade1d9c6fde87e8bbabb056fc4312aa6 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 1 Mar 2024 22:04:46 -0700 Subject: [PATCH 10/21] Document the file structure of the project (#232) Also customize styling of headers so that the smaller ones are a little larger. --- .../contributors/architecture/introduction.md | 2 + docs/contributors/architecture/structure.md | 375 ++++++++++++++++++ docs/stylesheets/extra.css | 20 + mkdocs.yml | 5 + 4 files changed, 402 insertions(+) create mode 100644 docs/contributors/architecture/structure.md create mode 100644 docs/stylesheets/extra.css diff --git a/docs/contributors/architecture/introduction.md b/docs/contributors/architecture/introduction.md index 3ca96d41..7e67209a 100644 --- a/docs/contributors/architecture/introduction.md +++ b/docs/contributors/architecture/introduction.md @@ -8,3 +8,5 @@ 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) 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/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/mkdocs.yml b/mkdocs.yml index e2fb439d..5b27ef76 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -31,12 +31,17 @@ nav: - "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: From 838d982758997003cf04c4ba7d8ff47421c6d5b0 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 1 Mar 2024 23:09:00 -0700 Subject: [PATCH 11/21] Correct references in architecture docs (#233) References to specific parts of the code are now invalid since the reorganization. --- .../architecture/how-rspec-works.md | 62 ++++++++++--------- .../architecture/how-super-diff-works.md | 50 +++++++-------- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/docs/contributors/architecture/how-rspec-works.md b/docs/contributors/architecture/how-rspec-works.md index e2c7240b..6c79e381 100644 --- a/docs/contributors/architecture/how-rspec-works.md +++ b/docs/contributors/architecture/how-rspec-works.md @@ -86,7 +86,7 @@ First, we will review several concepts in RSpec: [^fn1] 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) +- **[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". @@ -114,12 +114,14 @@ First, we will review several concepts in RSpec: [^fn1] 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 ()` + - `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, @@ -127,30 +129,30 @@ Given the above, RSpec performs the following sequence of events: 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`. + - 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, @@ -192,6 +194,8 @@ Given the above, RSpec performs the following sequence of events: 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 diff --git a/docs/contributors/architecture/how-super-diff-works.md b/docs/contributors/architecture/how-super-diff-works.md index 6a86ba85..2b6935df 100644 --- a/docs/contributors/architecture/how-super-diff-works.md +++ b/docs/contributors/architecture/how-super-diff-works.md @@ -2,17 +2,17 @@ ## SuperDiff's cast of characters -- An **inspection tree builder** (or, casually, an _inspector_) +- An **inspection tree builder** makes use of an **inspection tree** to generate a multi-line textual representation of an object, - similar to PrettyPrinter in Ruby or AwesomePrint, + 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**, + 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_, or _change_. + 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. @@ -33,9 +33,9 @@ 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` +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` +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`. @@ -60,7 +60,7 @@ in order to integrate fully with RSpec. 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` — + 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. @@ -88,7 +88,7 @@ Here are all of the places that SuperDiff patches RSpec: (to turn off syntax highlighting for code, as it interferes with the previous patches) - `RSpec::Support::ObjectFormatter` - (to use SuperDiff's object inspectors) + (to use SuperDiff's object inspection logic) - `RSpec::Matchers::ExpectedsForMultipleDiffs` (to add a key above the diff, add spacing around the diff, @@ -109,10 +109,11 @@ Once a test fails and RSpec delegates to SuperDiff's differ, this sequence of events occurs: -1. `SuperDiff::Differs::Main.call` is called with a pair of values: `expected` and `actual`. - This method looks for a differ that is suitable for the pair - among a set of defaults and the list of differs registered via SuperDiff's configuration. - It does this by calling `.applies_to?` on each, +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.) @@ -121,15 +122,15 @@ this sequence of events occurs: although this is sometimes overridden. 1. Once a differ is found, its `.call` method is called. - Since all differs inherit from `SuperDiff::Differs::Base`, + 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. + via the `#operation_tree_builder_class` method. For instance, - `SuperDiff::Differs::Array` uses a `SuperDiff::OperationTreeBuilder::Array`, - `SuperDiff::Differs::Hash` uses a `SuperDiff::OperationTreeBuilder::Hash`, + `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 @@ -140,15 +141,15 @@ this sequence of events occurs: find the differences between them, and represent those differences as operations. An operation may be one of four types: - `insert`, `delete`, `change`, or `noop`. + `: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::OperationTrees::Base`, + 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, @@ -165,12 +166,11 @@ this sequence of events occurs: 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`) + (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 a formatter — - so called `TieredLinesFormatter` — + 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. @@ -178,9 +178,9 @@ In summary: ```mermaid graph TB - DiffersMain["Differs::Main"] -- Differs --> Differ; + 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[Final diff]; + ElidedLines -- Tiered lines formatter --> FinalDiff[Diff string]; ``` From caa72abb2676477bffed36f349cad68c132e1544 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 1 Mar 2024 23:53:03 -0700 Subject: [PATCH 12/21] Add lint:changed and lint:changed:fix (#234) The Git hook that runs on push will only lint files changed within the current branch. However, it won't fix anything; to do that, you'll have to run `yarn lint:fix`. But actually, this runs every file through the linter, so it's fairly inefficient. This commit adds `lint:changed` and `lint:changed:fix` so that you can do the same thing that the Git hook does. --- .husky/pre-push | 17 +------ docs/contributors/how-to-contribute.md | 6 +++ package.json | 6 ++- scripts/lint-all-files.sh | 3 ++ scripts/lint-changed-files.sh | 64 ++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 18 deletions(-) create mode 100755 scripts/lint-all-files.sh create mode 100755 scripts/lint-changed-files.sh 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/docs/contributors/how-to-contribute.md b/docs/contributors/how-to-contribute.md index e0c1c86f..b9d27205 100644 --- a/docs/contributors/how-to-contribute.md +++ b/docs/contributors/how-to-contribute.md @@ -93,6 +93,12 @@ 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 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/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 "$@" From 8b971a852a3bf508b637316dffc0819544d173f5 Mon Sep 17 00:00:00 2001 From: Ben Kyriakou <74675306+benk-gc@users.noreply.github.com> Date: Thu, 25 Apr 2024 06:10:16 +0100 Subject: [PATCH 13/21] Support the use of primary keys other than "id" in ActiveRecord. (#237) Previously the diffing code made an assumption that "id" would always be the primary field. This is not always the case, and the use of `read_attribute(:id)` is deprecated in rails/rails@39997a0. This changes adds support for use of custom primary key fields, and updates the Person test model to use the "person_id" primary key. Note that this will almost certainly still run into issues if the model uses the newly introduced composite primary key type, so this would require additional changes outside the scope of this fix. --- .../inspection_tree_builders/active_record_model.rb | 12 ++++++++++-- lib/super_diff/active_record/monkey_patches.rb | 8 +++++--- .../operation_tree_builders/active_record_model.rb | 6 +++++- .../matchers/produce_output_when_run_matcher.rb | 2 +- spec/support/models/active_record/person.rb | 9 ++++++++- spec/support/shared_examples/active_record.rb | 10 +++++----- spec/unit/active_record/object_inspection_spec.rb | 10 +++++----- 7 files changed, 39 insertions(+), 18 deletions(-) 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 index 25bf2a2c..84929abf 100644 --- 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 @@ -6,6 +6,10 @@ 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( @@ -21,13 +25,17 @@ def call t1.nested do |t2| t2.insert_separated_list( - ["id"] + (object.attributes.keys.sort - ["id"]) + [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) + if name == id + t3.add_inspection_of object.id + else + t3.add_inspection_of object.read_attribute(name) + end end end diff --git a/lib/super_diff/active_record/monkey_patches.rb b/lib/super_diff/active_record/monkey_patches.rb index b2ebce7e..52ae6634 100644 --- a/lib/super_diff/active_record/monkey_patches.rb +++ b/lib/super_diff/active_record/monkey_patches.rb @@ -2,9 +2,11 @@ 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/operation_tree_builders/active_record_model.rb b/lib/super_diff/active_record/operation_tree_builders/active_record_model.rb index 72442b08..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 @@ -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/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 ), From db8c8bd1afcc09d8f3ca1c377ec9ac6dc49ab60c Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Wed, 24 Apr 2024 23:28:17 -0600 Subject: [PATCH 14/21] Add source_code_uri (+ other metadata) to gemspec (#239) This is necessary in order to get this gem to show up on tea.xyz. Source: --- super_diff.gemspec | 6 ++++++ 1 file changed, 6 insertions(+) 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/**/*"] From 8d1bee9833618d6e2951335440a0cc74b771ec71 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Wed, 24 Apr 2024 23:49:06 -0600 Subject: [PATCH 15/21] Release 0.12.0 (#240) --- CHANGELOG.md | 15 +++++++++++++++ lib/super_diff/version.rb | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 189f0ee0..3586015a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.12.0 - 2024-04-24 + +### 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 diff --git a/lib/super_diff/version.rb b/lib/super_diff/version.rb index 8f77097b..9459c337 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.0".freeze end From fc346d819bf07dc3da011e129ef2517aaa4d6048 Mon Sep 17 00:00:00 2001 From: sidane Date: Thu, 25 Apr 2024 19:02:39 +0100 Subject: [PATCH 16/21] Remove use of `pp` in DifferDispatcher (#242) Was causing the following output when running tests in Rails app using super_diff: ``` {:available_classes=> [SuperDiff::ActiveRecord::Differs::ActiveRecordRelation, SuperDiff::ActiveSupport::Differs::HashWithIndifferentAccess, SuperDiff::RSpec::Differs::CollectionContainingExactly, SuperDiff::RSpec::Differs::CollectionIncluding, SuperDiff::RSpec::Differs::HashIncluding, SuperDiff::RSpec::Differs::ObjectHavingAttributes, SuperDiff::Basic::Differs::Array, SuperDiff::Basic::Differs::Hash, SuperDiff::Basic::Differs::TimeLike, SuperDiff::Basic::Differs::DateLike, SuperDiff::Basic::Differs::MultilineString, SuperDiff::Basic::Differs::CustomObject, SuperDiff::Basic::Differs::DefaultObject]} ``` ![available_classes_output](https://github.com/mcmire/super_diff/assets/53722/78e5c98c-a5a7-43aa-8248-aff6263c2105) --- lib/super_diff/core/differ_dispatcher.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/super_diff/core/differ_dispatcher.rb b/lib/super_diff/core/differ_dispatcher.rb index 5321ff49..6f0a368c 100644 --- a/lib/super_diff/core/differ_dispatcher.rb +++ b/lib/super_diff/core/differ_dispatcher.rb @@ -10,8 +10,6 @@ class DifferDispatcher ) def call - pp available_classes: available_classes - if resolved_class resolved_class.call(expected, actual, indent_level: indent_level) elsif raise_if_nothing_applies? From 7d0305e0cc4ff154300607e2bd1e88e1c3527887 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 26 Apr 2024 21:05:03 -0600 Subject: [PATCH 17/21] Add Tea constitution file (#244) [Tea] will check for this file periodically to assign this gem to the correct codeowner. [Tea]: https://tea.xyz --- .prettierignore | 1 + tea.yaml | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 tea.yaml diff --git a/.prettierignore b/.prettierignore index 22e00978..e45057a8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,7 @@ .yarn/releases gemfiles site +tea.yaml tmp vendor/bundle yarn.lock 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 From cca0b34aba9a5376b4edc9ec87e9c1315e1d4689 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Apr 2024 04:27:15 +0000 Subject: [PATCH 18/21] Bump tar from 6.1.13 to 6.2.1 (#238) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 From 1a2fb6386cc7bf3273fd5858e22b6f6f20f25fa6 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 26 Apr 2024 22:35:28 -0600 Subject: [PATCH 19/21] Fix GH workflow so docs deploy on new releases (#245) Previously, when merging a new release, a new version of the docsite would not get deployed. Apparently I misunderstood the purpose of the `github.event.merged` property. I've changed this so that the step to determine the destination for the docsite (and hence whether the docsite should be deployed) looks for the `main` branch to be pushed and for `version.rb` to be updated. I've also fixed the index page for the docsite so it redirects to the correct release page. --- .github/workflows/super_diff.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/super_diff.yml b/.github/workflows/super_diff.yml index cf648fb9..0e144821 100644 --- a/.github/workflows/super_diff.yml +++ b/.github/workflows/super_diff.yml @@ -120,25 +120,25 @@ jobs: run: | set -x - if [[ "$IS_NEW_RELEASE" == "true" ]]; then + 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_CHANGES_TO_DOCS="true" + 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_CHANGES_TO_DOCS="false" + HAS_DOCS_CHANGES_TO_RELEASE="false" else - HAS_CHANGES_TO_DOCS="true" + HAS_DOCS_CHANGES_TO_RELEASE="true" fi fi { echo "DOCSITE_RELEASE_VERSION=$DOCSITE_RELEASE_VERSION" echo "DOCSITE_DESTINATION_PATH=$DOCSITE_DESTINATION_PATH" - echo "HAS_CHANGES_TO_DOCS=$HAS_CHANGES_TO_DOCS" + 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 }} @@ -148,7 +148,7 @@ jobs: outputs: DOCSITE_RELEASE_VERSION: ${{ steps.command.outputs.DOCSITE_RELEASE_VERSION }} DOCSITE_DESTINATION_PATH: ${{ steps.command.outputs.DOCSITE_DESTINATION_PATH }} - HAS_CHANGES_TO_DOCS: ${{ steps.command.outputs.HAS_CHANGES_TO_DOCS }} + HAS_DOCS_CHANGES_TO_RELEASE: ${{ steps.command.outputs.HAS_DOCS_CHANGES_TO_RELEASE }} build-docsite: runs-on: ubuntu-latest @@ -156,7 +156,7 @@ jobs: - analyze - collect-release-info - collect-docsite-release-info - if: ${{ github.event_name == 'pull_request' && ((needs.collect-release-info.outputs.IS_NEW_RELEASE == 'false' && needs.collect-docsite-release-info.outputs.HAS_CHANGES_TO_DOCS == 'true') || (needs.collect-release-info.outputs.IS_NEW_RELEASE == 'true' && github.event.merged)) }} + if: ${{ needs.collect-docsite-release-info.outputs.HAS_DOCS_CHANGES_TO_RELEASE == 'true' }} steps: - uses: actions/checkout@v4 - name: Install poetry @@ -178,6 +178,8 @@ jobs: 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 @@ -191,18 +193,19 @@ jobs: - 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. @@ -210,6 +213,8 @@ jobs: 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")" @@ -226,6 +231,7 @@ jobs: 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: From 4a3e3b3906f4ab537d5bc9d83a3ce40a07bf2b7c Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 26 Apr 2024 23:15:47 -0600 Subject: [PATCH 20/21] Fix link to Structure in docs (#247) --- docs/contributors/architecture/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributors/architecture/introduction.md b/docs/contributors/architecture/introduction.md index 7e67209a..4cd3d277 100644 --- a/docs/contributors/architecture/introduction.md +++ b/docs/contributors/architecture/introduction.md @@ -9,4 +9,4 @@ 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) document. +feel free to consult the [structure](./structure.md) document. From 92253b57854d36bd27fdb3c370fdb45df26c608f Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 26 Apr 2024 23:31:57 -0600 Subject: [PATCH 21/21] Release 0.12.1 (#246) --- CHANGELOG.md | 115 +++++++++++++++++++++++++++++++++++++- lib/super_diff/version.rb | 2 +- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3586015a..951aa85e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,119 @@ # Changelog -## 0.12.0 - 2024-04-24 +## 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 diff --git a/lib/super_diff/version.rb b/lib/super_diff/version.rb index 9459c337..90a130e0 100644 --- a/lib/super_diff/version.rb +++ b/lib/super_diff/version.rb @@ -1,3 +1,3 @@ module SuperDiff - VERSION = "0.12.0".freeze + VERSION = "0.12.1".freeze end