pax_global_header00006660000000000000000000000064141422771650014523gustar00rootroot0000000000000052 comment=02b528a0e39a869edc786db17dee8dd2aa9a0fd7 hashie-5.0.0/000077500000000000000000000000001414227716500127665ustar00rootroot00000000000000hashie-5.0.0/.github/000077500000000000000000000000001414227716500143265ustar00rootroot00000000000000hashie-5.0.0/.github/workflows/000077500000000000000000000000001414227716500163635ustar00rootroot00000000000000hashie-5.0.0/.github/workflows/main.yml000066400000000000000000000042301414227716500200310ustar00rootroot00000000000000name: CI on: push: branches: - master pull_request: branches: - master jobs: danger: runs-on: ubuntu-latest if: ${{ github.event_name == 'pull_request' }} steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.7 bundler-cache: true - uses: MeilCli/danger-action@v5 with: danger_file: Dangerfile danger_id: danger-pr install_path: vendor/bundle plugins_file: Gemfile env: DANGER_GITHUB_API_TOKEN: ${{ secrets.github_token }} integration-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true ruby-version: 2.7 - name: Run integration tests run: | for dir in spec/integration/*; do echo "testing $dir integration" BUNDLE_GEMFILE=$dir/Gemfile bundle install --jobs 4 --retry 3 BUNDLE_GEMFILE=$dir/Gemfile bundle exec rspec $dir done test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby: - 3.0 - 2.7 - 2.6 - 2.5 - 2.4 - 2.3 - 2.2 - 2.1 steps: - uses: actions/checkout@v2 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true ruby-version: ${{ matrix.ruby }} - name: Install dependencies run: bundle install --jobs 4 --retry 3 - name: Run tests run: bundle exec rake test-jruby: runs-on: ubuntu-latest strategy: fail-fast: false matrix: os: - macos - ubuntu jruby: - jruby steps: - uses: actions/checkout@v2 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true ruby-version: ${{ matrix.jruby }} - name: Install dependencies env: JRUBY_OPTS: --debug run: bundle install --jobs 4 --retry 3 - name: Run tests env: JRUBY_OPTS: --debug run: bundle exec rake hashie-5.0.0/.gitignore000066400000000000000000000001171414227716500147550ustar00rootroot00000000000000*.sw? .DS_Store coverage rdoc pkg *.gem *.log .bundle .rvmrc Gemfile.lock log/ hashie-5.0.0/.rspec000066400000000000000000000001221414227716500140760ustar00rootroot00000000000000--colour --format=documentation --exclude-pattern='spec/integration/**/*_spec.rb' hashie-5.0.0/.rubocop.yml000066400000000000000000000015651414227716500152470ustar00rootroot00000000000000AllCops: Include: - Guardfile - Rakefile Exclude: - .bundle/**/* - vendor/**/* inherit_from: .rubocop_todo.yml # Disabled until we can use the squiggly heredoc (after deprecating Ruby <2.3) Layout/IndentHeredoc: Enabled: false Metrics/ClassLength: Enabled: false Metrics/ModuleLength: Enabled: false Metrics/BlockLength: Exclude: - 'spec/**/*.rb' Metrics/LineLength: Exclude: - 'Guardfile' Max: 100 Lint/UnifiedInteger: Exclude: - 'lib/hashie/extensions/coercion.rb' - 'spec/hashie/extensions/coercion_spec.rb' Naming/FileName: Exclude: - 'Dangerfile' - '**/Gemfile' - '**/Rakefile' # Disabled because of the very generic nature of Hashie. In the cases where we # use double negation, there isn't a means that as succinct of doing the type of # boolean casting that we do. Style/DoubleNegation: Enabled: false hashie-5.0.0/.rubocop_todo.yml000066400000000000000000000013071414227716500162660ustar00rootroot00000000000000# This configuration was generated by # `rubocop --auto-gen-config` # on 2020-10-02 23:12:27 -0300 using RuboCop version 0.52.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 9 Metrics/AbcSize: Max: 23 # Offense count: 8 Metrics/CyclomaticComplexity: Max: 11 # Offense count: 18 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 28 # Offense count: 6 Metrics/PerceivedComplexity: Max: 10 # Offense count: 44 Style/Documentation: Enabled: false hashie-5.0.0/.yardopts000066400000000000000000000000141414227716500146270ustar00rootroot00000000000000-m markdown hashie-5.0.0/CHANGELOG.md000066400000000000000000001057241414227716500146100ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Any violations of this scheme are considered to be bugs. ## [5.0.0] - 2021-11-08 [5.0.0]: https://github.com/hashie/hashie/compare/v4.1.0...v5.0.0 ### Added * [#523](https://github.com/hashie/hashie/pull/523): Added TOC, ensure a keep-a-changelog formatted CHANGELOG - [@dblock](https://github.com/dblock). * [#522](https://github.com/hashie/hashie/pull/522): Added eierlegende Wollmilchsau mascot graphic - [@carolineartz](https://github.com/carolineartz). * [#530](https://github.com/hashie/hashie/pull/530): Added Hashie::Extensions::Dash::PredefinedValues - [@caalberts](https://github.com/caalberts). * [#536](https://github.com/hashie/hashie/pull/536): Added exporting a normal Hash from an indifferent one through the `#to_hash` method - [@michaelherold](https://github.com/michaelherold). * [#539](https://github.com/hashie/hashie/pull/539): Run 2.7 tests once - [@anakinj](https://github.com/anakinj). ### Changed * [#521](https://github.com/hashie/hashie/pull/499): Do not convert keys that cannot be represented as symbols to `String` in `Mash` initialization - [@carolineartz](https://github.com/carolineartz). * [#524](https://github.com/hashie/hashie/pull/524): Test with Ruby 2.7 - [@aried3r](https://github.com/aried3r). * [#525](https://github.com/hashie/hashie/pull/525): Use `indifferent_writer` in `IndifferentAccess#convert!` - [@yogeshjain999](https://github.com/yogeshjain999). * [#527](https://github.com/hashie/hashie/pull/527): Updated Copyright to (c) 2009-2020 Intridea, Inc., and Contributors - [@dblock](https://github.com/dblock). * [#555](https://github.com/hashie/hashie/pull/555): Test with Ruby 3.0 - [@dblock](https://github.com/dblock). ### Removed * [#538](https://github.com/hashie/hashie/pull/538): Dropped testing for JRuby 9.0, though not support - [@michaelherold](https://github.com/michaelherold). ### Fixed * [#516](https://github.com/hashie/hashie/issues/516): Fixed `NoMethodError` raised when including `Hashie::Extensions::Mash::SymbolizeKeys` and `Hashie::Extensions::SymbolizeKeys` in mashes/hashes with non string or symbol keys - [@carolineartz](https://github.com/carolineartz). * [#531](https://github.com/hashie/hashie/pull/531): Fixed [slice doesn't work using symbols](https://github.com/hashie/hashie/issues/529) using hash with `IndifferentAccess` extension - [@gnomex](https://github.com/gnomex). * [#533](https://github.com/hashie/hashie/pull/533): Fixed `NoMethodError: undefined method 'to_json'` at `hashie/dash_spec` - [@gnomex](https://github.com/gnomex). * [#535](https://github.com/hashie/hashie/pull/535): Restored the exporting of all properties as part of `Dash#to_h` and `Dash#to_hash` - [@michaelherold](https://github.com/michaelherold). * [#537](https://github.com/hashie/hashie/pull/537): Fixed inconsistencies with handling defaults in `Dash` with and without `IgnoreUnclared` mixed in - [@michaelherold](https://github.com/michaelherold). * [#547](https://github.com/hashie/hashie/pull/547): Fixed issue where a source hash key can be used in translating multiple properties - [@danwa5](https://github.com/danwa5). ## [4.1.0] - 2020-02-01 [4.1.0]: https://github.com/hashie/hashie/compare/v4.0.0...v4.1.0 ### Added * [#545](https://github.com/hashie/hashie/pull/545): Add `Hashie::Mash#except` and `Hashie::Extensions::IndifferentAccess#except` when running under Ruby 3 to match newly added Ruby stdlib method - [@jackjennings](https://github.com/jackjennings). * [#499](https://github.com/hashie/hashie/pull/499): Add `Hashie::Extensions::Mash::PermissiveRespondTo` to make specific subclasses of Mash fully respond to messages for use with `SimpleDelegator` - [@michaelherold](https://github.com/michaelherold). ### Changed * [#498](https://github.com/hashie/hashie/pull/498): Exclude tests from the gem release to reduce installation size and improve installation speed - [@michaelherold](https://github.com/michaelherold). ### Fixed * [#467](https://github.com/intridea/hashie/pull/467): Fixed `DeepMerge#deep_merge` mutating nested values within the receiver - [@michaelherold](https://github.com/michaelherold). * [#505](https://github.com/hashie/hashie/pull/505): Ensure that `Hashie::Array`s are not deconverted within `Hashie::Mash`es to make `Mash#dig` work properly - [@michaelherold](https://github.com/michaelherold). * [#507](https://github.com/hashie/hashie/pull/507): Suppress `Psych.safe_load` arg warn when using Psych 3.1.0+ - [@koic](https://github.com/koic). * [#508](https://github.com/hashie/hashie/pull/508): Fixed `Mash.load` no longer uses Rails-only `#except` - [@bobbymcwho](https://github.com/bobbymcwho). * [#508](https://github.com/hashie/hashie/pull/508): Fixed `Hashie::Extensions::DeepMerge` `#deep_merge` not correctly dup'ing sub-hashes if active_support hash extensions were not present - [@bobbymcwho](https://github.com/bobbymcwho). * [#500](https://github.com/hashie/hashie/pull/500): Do not warn when setting Mash keys that look like underbang, bang, and query methods - [@michaelherold](https://github.com/michaelherold). * [#510](https://github.com/hashie/hashie/pull/510): Ensure that `Hashie::Mash#compact` is only defined on Ruby version >= 2.4.0 - [@bobbymcwho](https://github.com/bobbymcwho). * [#511](https://github.com/hashie/hashie/pull/511): Suppress keyword arguments warning for Ruby 2.7.0 - [@koic](https://github.com/koic). * [#512](https://github.com/hashie/hashie/pull/512): Suppress an integer unification warning for using Ruby 2.4.0+ - [@koic](https://github.com/koic). * [#513](https://github.com/hashie/hashie/pull/513): Suppress a Ruby's warning when using Ruby 2.6.0+ - [@koic](https://github.com/koic). ## [4.0.0] - 2019-10-30 [4.0.0]: https://github.com/hashie/hashie/compare/v3.6.0...v4.0.0 ### Added * [#323](https://github.com/hashie/hashie/pull/323): Added `Hashie::Extensions::Mash::DefineAccessors` - [@marshall-lee](https://github.com/marshall-lee). * [#474](https://github.com/hashie/hashie/pull/474): Expose `YAML#safe_load` options in `Mash#load` - [@riouruma](https://github.com/riouruma), [@dblock](https://github.com/dblock). * [#478](https://github.com/hashie/hashie/pull/478): Added optional array parameter to `Hashie::Mash.disable_warnings` - [@bobbymcwho](https://github.com/bobbymcwho). * [#481](https://github.com/hashie/hashie/pull/481): Ruby 2.6 - Support `Hash#merge` and `#merge!` called with multiple Hashes/Mashes - [@bobbymcwho](https://github.com/bobbymcwho). * [#488](https://github.com/hashie/hashie/pull/488): Added ability to create an anonymous `Hashie::Mash` subclass with key conflict errors silenced using `Hashie::Mash.quiet.new` - [@bobbymcwho](https://github.com/bobbymcwho). ### Changed * [#481](https://github.com/hashie/hashie/pull/481): Implement non-destructive standard Hash methods - [@bobbymcwho](https://github.com/bobbymcwho). * [#482](https://github.com/hashie/hashie/pull/482): Update Travis configs to make jruby builds run on trusty dist - [@BobbyMcWho](https://github.com/BobbyMcWho). ### Fixed * [#459](https://github.com/hashie/hashie/pull/459): Fixed a regression in `Mash.load` that disallowed aliases - [@arekt](https://github.com/arekt) and [@michaelherold](https://github.com/michaelherold). * [#465](https://github.com/hashie/hashie/pull/465): Fixed `deep_update` to call any readers when a key exists - [@laertispappas](https://github.com/laertispappas). * [#479](https://github.com/hashie/hashie/pull/479): Fixed an issue with `Hash#except` not returning a `Mash` in Rails 6 - [@bobbymcwho](https://github.com/bobbymcwho). * [#489](https://github.com/hashie/hashie/pull/489): Updated the documentation to exlain the behavior of `Mash` and keyword arguments - [@Bhacaz](https://github.com/Bhacaz). * [#465](https://github.com/hashie/hashie/pull/465): Clean up our RuboCop configuration and fix the outstanding line length violations. This involved some minor refactoring on `Hashie::Extensions::Coercion`, `Hashie::Extensions::Dash::IndifferentAccess`, `Hashie::Extensions::DeepLocate`, `Hashie::Extensions::Mash::SafeAssignment`, and `Hashie::Hash`, but none that were detectable via the test suite - [@michaelherold](https://github.com/michaelherold). ## [3.6.0] - 2018-08-13 [3.6.0]: https://github.com/hashie/hashie/compare/v3.5.7...v3.6.0 ### Added * [#455](https://github.com/hashie/hashie/pull/455): Allow overriding methods when passing in a hash - [@lnestor](https://github.com/lnestor). * [#434](https://github.com/hashie/hashie/pull/434): Add documentation around Mash sub-Hashes - [@michaelherold](https://github.com/michaelherold). * [#439](https://github.com/hashie/hashie/pull/439): Add an integration spec for Elasticsearch - [@michaelherold](https://github.com/michaelherold). ### Changed * [#433](https://github.com/hashie/hashie/pull/433): Update Rubocop to the most recent version - [@michaelherold](https://github.com/michaelherold). ### Fixed * [#435](https://github.com/hashie/hashie/pull/435): Mash `default_proc`s are now propagated down to nested sub-Hashes - [@michaelherold](https://github.com/michaelherold). * [#436](https://github.com/hashie/hashie/pull/436): Ensure that `Hashie::Extensions::IndifferentAccess` injects itself after a non-destructive merge - [@michaelherold](https://github.com/michaelherold). * [#437](https://github.com/hashie/hashie/pull/437): Allow codependent properties to be set on Dash - [@michaelherold](https://github.com/michaelherold). * [#438](https://github.com/hashie/hashie/pull/438): Fix: `NameError (uninitialized constant Hashie::Extensions::Parsers::YamlErbParser::Pathname)` in `Hashie::Mash.load` - [@onk](https://github.com/onk). * [#457](https://github.com/hashie/hashie/pull/457): Fix `Trash` to allow it to copy properties from other properties - [@michaelherold](https://github.com/michaelherold). ## [3.5.7] - 2017-12-19 [3.5.7]: https://github.com/hashie/hashie/compare/v3.5.6...v3.5.7 ### Fixed * [#430](https://github.com/hashie/hashie/pull/430): Fix Hashie::Rash randomly losing items - [@Antti](https://github.com/Antti). ### Changed * [#425](https://github.com/hashie/hashie/pull/425): Update rubies in CI - [@kachick](https://github.com/kachick). ## [3.5.6] - 2017-07-12 [3.5.6]: https://github.com/hashie/hashie/compare/v3.5.5...v3.5.6 ### Fixed * [#416](https://github.com/hashie/hashie/pull/416): Fix `warning: instance variable @disable_warnings not initialized` - [@axfcampos](https://github.com/axfcampos). ## [3.5.5] - 2017-02-24 [3.5.5]: https://github.com/hashie/hashie/compare/v3.5.4...v3.5.5 ### Added * [#326](https://github.com/hashie/hashie/pull/326): Added `Hashie::Extensions::Mash::KeepOriginalKeys` to give Mashes the ability to keep the original structure given to it - [@michaelherold](https://github.com/michaelherold). ### Fixed * [#415](https://github.com/hashie/hashie/pull/415): Fixed Mash logging keys multiple times which lead to a bad user experience or, in some cases, errors - [@michaelherold](https://github.com/michaelherold). ## [3.5.4] - 2017-02-22 [3.5.4]: https://github.com/hashie/hashie/compare/v3.5.3...v3.5.4 ### Added * [#412](https://github.com/hashie/hashie/pull/412): Added a Hashie::Extensions::Mash::SymbolizeKeys extension that overrides the default stringification behavior for keys - [@michaelherold](https://github.com/michaelherold). ### Fixed * [#409](https://github.com/hashie/hashie/pull/409): Fixed Railtie detection for projects where Rails is defined but Railties are not availble - [@CallumD](https://github.com/callumd). * [#411](https://github.com/hashie/hashie/pull/411): Fixed a performance regression from 3.4.3 that caused a 10x slowdown in OmniAuth - [@michaelherold](https://github.com/michaelherold). ## [3.5.3] - 2017-02-11 [3.5.3]: https://github.com/hashie/hashie/compare/v3.5.2...v3.5.3 ### Fixed * [#402](https://github.com/hashie/hashie/pull/402): Use a Railtie to set Hashie.logger on rails boot - [@matthewrudy](https://github.com/matthewrudy). * [#406](https://github.com/hashie/hashie/pull/406): Ensure that subclasses that disable warnings propagate that setting to grandchild classes - [@michaelherold](https://github.com/michaelherold). * Your contribution here. ## [3.5.2] - 2017-02-10 [3.5.2]: https://github.com/hashie/hashie/compare/v3.5.1...v3.5.2 ### Added * [#395](https://github.com/hashie/hashie/pull/395): Add the ability to disable warnings in Mash subclasses - [@michaelherold](https://github.com/michaelherold). * [#400](https://github.com/hashie/hashie/pull/400): Fix Hashie.logger load and set the Hashie logger to the Rails logger in a Rails environment - [@michaelherold](https://github.com/michaelherold). * [#397](https://github.com/hashie/hashie/pull/397): Add the integration specs harness into the main test tasks - [@michaelherold](https://github.com/michaelherold). ### Fixed * [#396](https://github.com/hashie/hashie/pull/396): Fix for specs in #381: Incorrect use of shared context meant example was not being run - [@biinari](https://github.com/biinari). * [#399](https://github.com/hashie/hashie/pull/399): Fix passing Pathname object to Hashie::Mesh.load() - [@albb0920](https://github.com/albb0920). ## [3.5.1] - 2017-01-31 * [#392](https://github.com/hashie/hashie/pull/392): Fix for #391: Require all dependencies of Hashie::Mash - [@dblock](https://github.com/dblock). [3.5.1]: https://github.com/hashie/hashie/compare/v3.5.0...v3.5.1 ## [3.5.0] - 2017-01-31 * [#386](https://github.com/hashie/hashie/pull/386): Fix for #385: Make `deep_merge` always `deep_dup` nested hashes before merging them in so that there are no shared references between the two hashes being merged. - [@mltsy](https://github.com/mltsy). * [#389](https://github.com/hashie/hashie/pull/389): Support Ruby 2.4.0 - [@camelmasa](https://github.com/camelmasa). [3.5.0]: https://github.com/hashie/hashie/compare/v3.4.6...v3.5.0 ### Added * [#381](https://github.com/hashie/hashie/pull/381): Add a logging layer that lets us report potential issues to our users. As the first logged issue, report when a `Hashie::Mash` is attempting to overwrite a built-in method, since that is one of our number one questions - [@michaelherold](https://github.com/michaelherold). ### Changed * [#384](https://github.com/hashie/hashie/pull/384): Updated to CodeClimate 1.x - [@boffbowsh](https://github.com/boffbowsh). ### Fixed * [#369](https://github.com/hashie/hashie/pull/369): If a translation for a property exists when using IndifferentAccess and IgnoreUndeclared, use the translation to find the property - [@whitethunder](https://github.com/whitethunder). * [#376](https://github.com/hashie/hashie/pull/376): Leave string index unchanged if it can't be converted to integer for Array#dig - [@sazor](https://github.com/sazor). * [#377](https://github.com/hashie/hashie/pull/377): Dont use Rubygems to check ruby version - [@sazor](https://github.com/sazor). * [#378](https://github.com/hashie/hashie/pull/378): Deep find all searches inside all nested hashes - [@sazor](https://github.com/sazor). * [#380](https://github.com/hashie/hashie/pull/380): Evaluate procs default values of Dash in object initialization - [@sazor](https://github.com/sazor). * [#387](https://github.com/hashie/hashie/pull/387): Fixed builds failing due to Rake 11 having a breaking change - [@michaelherold](https://github.com/michaelherold). ## [3.4.6] - 2016-09-16 [3.4.6]: https://github.com/hashie/hashie/compare/v3.4.5...v3.4.6 ### Fixed * [#368](https://github.com/hashie/hashie/pull/368): Since `hashie/mash` can be required alone, require its dependencies - [@jrafanie](https://github.com/jrafanie). ## [3.4.5] - 2016-09-16 [3.4.5]: https://github.com/hashie/hashie/compare/v3.4.4...v3.4.5 ### Added * [#337](https://github.com/hashie/hashie/pull/337), [#331](https://github.com/hashie/hashie/issues/331): `Hashie::Mash#load` accepts a `Pathname` object - [@gipcompany](https://github.com/gipcompany). * [#366](https://github.com/hashie/hashie/pull/366): Added Danger, PR linter - [@dblock](https://github.com/dblock). ### Deprecated * [#366](https://github.com/hashie/hashie/pull/366): Hashie is no longer tested on Ruby < 2 - [@dblock](https://github.com/dblock). ### Fixed * [#358](https://github.com/hashie/hashie/pull/358): Fixed support for Array#dig - [@modosc](https://github.com/modosc). * [#365](https://github.com/hashie/hashie/pull/365): Ensured ActiveSupport::HashWithIndifferentAccess is defined before use in #deep_locate - [@mikejarema](https://github.com/mikejarema). ## [3.4.4] - 2016-04-29 [3.4.4]: https://github.com/hashie/hashie/compare/v3.4.3...v3.4.4 ### Added * [#349](https://github.com/hashie/hashie/pull/349): Convert `Hashie::Mash#dig` arguments for Ruby 2.3.0 - [@k0kubun](https://github.com/k0kubun). ### Fixed * [#240](https://github.com/hashie/hashie/pull/240): Fixed nesting twice with Clash keys - [@bartoszkopinski](https://github.com/bartoszkopinski). * [#317](https://github.com/hashie/hashie/pull/317): Ensured `Hashie::Extensions::MethodQuery` methods return boolean values - [@michaelherold](https://github.com/michaelherold). * [#319](https://github.com/hashie/hashie/pull/319): Fixed a regression from 3.4.1 where `Hashie::Extensions::DeepFind` is no longer indifference-aware - [@michaelherold](https://github.com/michaelherold). * [#322](https://github.com/hashie/hashie/pull/322): Fixed `reverse_merge` issue with `Mash` subclasses - [@marshall-lee](https://github.com/marshall-lee). * [#346](https://github.com/hashie/hashie/pull/346): Fixed `merge` breaking indifferent access - [@docwhat](https://github.com/docwhat), [@michaelherold](https://github.com/michaelherold). * [#350](https://github.com/hashie/hashie/pull/350): Fixed from string translations used with `IgnoreUndeclared` - [@marshall-lee](https://github.com/marshall-lee). ## [3.4.3] - 2015-10-25 [3.4.3]: https://github.com/hashie/hashie/compare/v3.4.2...v3.4.3 ### Added * [#306](https://github.com/hashie/hashie/pull/306): Added `Hashie::Extensions::Dash::Coercion` - [@marshall-lee](https://github.com/marshall-lee). * [#314](https://github.com/hashie/hashie/pull/314): Added a `StrictKeyAccess` extension that will raise an error whenever a key is accessed that does not exist in the hash - [@pboling](https://github.com/pboling). ### Fixed * [#304](https://github.com/hashie/hashie/pull/304): Ensured compatibility of `Hash` extensions with singleton objects - [@regexident](https://github.com/regexident). * [#310](https://github.com/hashie/hashie/pull/310): Fixed `Hashie::Extensions::SafeAssignment` bug with private methods - [@marshall-lee](https://github.com/marshall-lee). ### Changed * [#313](https://github.com/hashie/hashie/pull/313): Restrict pending spec to only Ruby versions 2.2.0-2.2.2 - [@pboling](https://github.com/pboling). * [#315](https://github.com/hashie/hashie/pull/315): Default `bin/` scripts: `console` and `setup` - [@pboling](https://github.com/pboling). ## [3.4.2] - 2015-06-02 [3.4.2]: https://github.com/hashie/hashie/compare/v3.4.1...v3.4.2 ### Added * [#297](https://github.com/hashie/hashie/pull/297): Extracted `Trash`'s behavior into a new `Dash::PropertyTranslation` extension - [@michaelherold](https://github.com/michaelherold). ### Removed * [#292](https://github.com/hashie/hashie/pull/292): Removed `Mash#id` and `Mash#type` - [@jrochkind](https://github.com/jrochkind). ## [3.4.1] - 2015-03-31 [3.4.1]: https://github.com/hashie/hashie/compare/v3.4.0...v3.4.1 ### Added * [#269](https://github.com/hashie/hashie/pull/272): Added Hashie::Extensions::DeepLocate - [@msievers](https://github.com/msievers). * [#281](https://github.com/hashie/hashie/pull/281): Added #reverse_merge to Mash to override ActiveSupport's version - [@mgold](https://github.com/mgold). ### Fixed * [#270](https://github.com/hashie/hashie/pull/277): Fixed ArgumentError raised when using IndifferentAccess and HashWithIndifferentAccess - [@gardenofwine](https://github.com/gardenofwine). * [#282](https://github.com/hashie/hashie/pull/282): Fixed coercions in a subclass accumulating in the superclass - [@maxlinc](https://github.com/maxlinc), [@martinstreicher](https://github.com/martinstreicher). ## [3.4.0] - 2015-02-02 [3.4.0]: https://github.com/hashie/hashie/compare/v3.3.2...v3.4.0 ### Added * [#251](https://github.com/hashie/hashie/pull/251): Added block support to indifferent access #fetch - [@jgraichen](https://github.com/jgraichen). * [#252](https://github.com/hashie/hashie/pull/252): Added support for conditionally required Hashie::Dash attributes - [@ccashwell](https://github.com/ccashwell). * [#254](https://github.com/hashie/hashie/pull/254): Added public utility methods for stringify and symbolize keys - [@maxlinc](https://github.com/maxlinc). * [#260](https://github.com/hashie/hashie/pull/260): Added block support to Extensions::DeepMerge - [@galathius](https://github.com/galathius). * [#271](https://github.com/hashie/hashie/pull/271): Added ability to define defaults based on current hash - [@gregory](https://github.com/gregory). ### Changed * [#249](https://github.com/hashie/hashie/pull/249): SafeAssignment will now also protect hash-style assignments - [@jrochkind](https://github.com/jrochkind). * [#264](https://github.com/hashie/hashie/pull/264): Methods such as abc? return true/false with Hashie::Extensions::MethodReader - [@Zloy](https://github.com/Zloy). ### Fixed * [#247](https://github.com/hashie/hashie/pull/247): Fixed #stringify_keys and #symbolize_keys collision with ActiveSupport - [@bartoszkopinski](https://github.com/bartoszkopinski). * [#256](https://github.com/hashie/hashie/pull/256): Inherited key coercions - [@Erol](https://github.com/Erol). * [#259](https://github.com/hashie/hashie/pull/259): Fixed handling of default proc values in Mash - [@Erol](https://github.com/Erol). * [#261](https://github.com/hashie/hashie/pull/261): Fixed bug where Dash.property modifies argument object - [@d-tw](https://github.com/d-tw). * [#269](https://github.com/hashie/hashie/pull/269): Added #extractable_options? so ActiveSupport Array#extract_options! can extract it - [@ridiculous](https://github.com/ridiculous). ## [3.3.2] - 2014-11-26 [3.3.2]: https://github.com/hashie/hashie/compare/v3.3.1...v3.3.2 ### Added * [#231](https://github.com/hashie/hashie/pull/231): Added support for coercion on class type that inherit from Hash - [@gregory](https://github.com/gregory). * [#233](https://github.com/hashie/hashie/pull/233): Custom error messages for required properties in Hashie::Dash subclasses - [@joss](https://github.com/joss). * [#245](https://github.com/hashie/hashie/pull/245): Added Hashie::Extensions::MethodAccessWithOverride to autoloads - [@Fritzinger](https://github.com/Fritzinger). ### Fixed * [#221](https://github.com/hashie/hashie/pull/221): Reduced amount of allocated objects on calls with suffixes in Hashie::Mash - [@kubum](https://github.com/kubum). * [#224](https://github.com/hashie/hashie/pull/224): Merging Hashie::Mash now correctly only calls the block on duplicate values - [@amysutedja](https://github.com/amysutedja). * [#228](https://github.com/hashie/hashie/pull/228): Made Hashie::Extensions::Parsers::YamlErbParser pass template filename to ERB - [@jperville](https://github.com/jperville). ## [3.3.1] - 2014-08-26 [3.3.1]: https://github.com/hashie/hashie/compare/v3.3.0...v3.3.1 ### Added * [#183](https://github.com/hashie/hashie/pull/183): Added Mash#load with YAML file support - [@gregory](https://github.com/gregory). * [#189](https://github.com/hashie/hashie/pull/189): Added Rash#fetch - [@medcat](https://github.com/medcat). * [#204](https://github.com/hashie/hashie/pull/204): Added Hashie::Extensions::MethodOverridingWriter and MethodAccessWithOverride - [@michaelherold](https://github.com/michaelherold). * [#205](https://github.com/hashie/hashie/pull/205): Added Hashie::Extensions::Mash::SafeAssignment - [@michaelherold](https://github.com/michaelherold). * [#209](https://github.com/hashie/hashie/pull/209): Added Hashie::Extensions::DeepFind - [@michaelherold](https://github.com/michaelherold). ### Fixed * [#69](https://github.com/hashie/hashie/pull/69): Fixed regression in assigning multiple properties in Hashie::Trash - [@michaelherold](https://github.com/michaelherold), [@einzige](https://github.com/einzige), [@dblock](https://github.com/dblock). * [#195](https://github.com/hashie/hashie/pull/195): Ensured that the same object is returned after injecting IndifferentAccess - [@michaelherold](https://github.com/michaelherold). * [#201](https://github.com/hashie/hashie/pull/201): Hashie::Trash transforms can be inherited - [@fobocaster](https://github.com/fobocaster). * [#200](https://github.com/hashie/hashie/pull/200): Improved coercion: primitives and error handling - [@maxlinc](https://github.com/maxlinc). * [#206](https://github.com/hashie/hashie/pull/206): Fixed stack overflow from repetitively including coercion in subclasses - [@michaelherold](https://github.com/michaelherold). * [#207](https://github.com/hashie/hashie/pull/207): Fixed inheritance of transformations in Trash - [@fobocaster](https://github.com/fobocaster). ## [3.2.0] - 2014-07-10 [3.2.0]: https://github.com/hashie/hashie/compare/v3.1.0...v3.2.0 ### Added * [#177](https://github.com/hashie/hashie/pull/177): Added support for coercing enumerables and collections - [@gregory](https://github.com/gregory). ### Changed * [#179](https://github.com/hashie/hashie/pull/179): Mash#values_at will convert each key before doing the lookup - [@nahiluhmot](https://github.com/nahiluhmot). * [#184](https://github.com/hashie/hashie/pull/184): Allow ranges on Rash to match all Numeric types - [@medcat](https://github.com/medcat). ### Fixed * [#164](https://github.com/hashie/hashie/pull/164), [#165](https://github.com/hashie/hashie/pull/165), [#166](https://github.com/hashie/hashie/pull/166): Fixed stack overflow when coercing mashes that contain ActiveSupport::HashWithIndifferentAccess values - [@numinit](https://github.com/numinit), [@kgrz](https://github.com/kgrz). * [#187](https://github.com/hashie/hashie/pull/187): Automatically require version - [@medcat](https://github.com/medcat). * [#190](https://github.com/hashie/hashie/issues/190): Fixed `coerce_key` with `from` Trash feature and Coercion extension - [@gregory](https://github.com/gregory). * [#192](https://github.com/hashie/hashie/pull/192): Fixed StringifyKeys#stringify_keys! to recursively stringify keys of embedded ::Hash types - [@dblock](https://github.com/dblock). ## [3.1.0] - 2014-06-25 [3.1.0]: https://github.com/hashie/hashie/compare/v3.0.0...v3.1.0 ### Added * [#172](https://github.com/hashie/hashie/pull/172): Added Dash and Trash#update_attributes! - [@gregory](https://github.com/gregory). ### Changed * [#169](https://github.com/hashie/hashie/pull/169): Hash#to_hash will also convert nested objects that implement to_hash - [@gregory](https://github.com/gregory). * [#173](https://github.com/hashie/hashie/pull/173): Auto include Dash::IndifferentAccess when IndifferentAccess is included in Dash - [@gregory](https://github.com/gregory). ### Fixed * [#171](https://github.com/hashie/hashie/pull/171): Include Trash and Dash class name when raising `NoMethodError` - [@gregory](https://github.com/gregory). * [#174](https://github.com/hashie/hashie/pull/174): Fixed `from` and `transform_with` Trash features when IndifferentAccess is included - [@gregory](https://github.com/gregory). ## [3.0.0] - 2014-06-03 [3.0.0]: https://github.com/hashie/hashie/compare/v2.1.2...v3.0.0 ### Added * [#149](https://github.com/hashie/hashie/issues/149): Allow IgnoreUndeclared and DeepMerge to be used with undeclared properties - [@jhaesus](https://github.com/jhaesus). ### Changed * [#152](https://github.com/hashie/hashie/pull/152): Do not convert keys to String in Hashie::Dash and Hashie::Trash, use Hashie::Extensions::Dash::IndifferentAccess to achieve backward compatible behavior - [@dblock](https://github.com/dblock). * [#152](https://github.com/hashie/hashie/pull/152): Do not automatically stringify keys in Hashie::Hash#to_hash, pass `:stringify_keys` to achieve backward compatible behavior - [@dblock](https://github.com/dblock). ### Fixed * [#146](https://github.com/hashie/hashie/issues/146): Mash#respond_to? inconsistent with #method_missing and does not respond to #permitted? - [@dblock](https://github.com/dblock). * [#148](https://github.com/hashie/hashie/pull/148): Consolidated Hashie::Hash#stringify_keys implementation - [@dblock](https://github.com/dblock). * [#159](https://github.com/hashie/hashie/pull/159): Handle nil intermediate object on deep fetch - [@stephenaument](https://github.com/stephenaument). ## [2.1.2] - 2014-05-12 [2.1.2]: https://github.com/hashie/hashie/compare/v2.1.1...v2.1.2 ### Changed * [#169](https://github.com/hashie/hashie/pull/169): Hash#to_hash will also convert nested objects that implement `to_hash` - [@gregory](https://github.com/gregory). ## [2.1.1] - 2014-04-12 [2.1.1]: https://github.com/hashie/hashie/compare/v2.1.0...v2.1.1 ### Fixed * [#131](https://github.com/hashie/hashie/pull/131): Added IgnoreUndeclared, an extension to silently ignore undeclared properties at intialization - [@righi](https://github.com/righi). * [#138](https://github.com/hashie/hashie/pull/138): Added Hashie::Rash, a hash whose keys can be regular expressions or ranges - [@epitron](https://github.com/epitron). * [#144](https://github.com/hashie/hashie/issues/144): Fixed regression invoking `to_hash` with no parameters - [@mbleigh](https://github.com/mbleigh). ## [2.1.0] - 2014-04-06 [2.1.0]: https://github.com/hashie/hashie/compare/v2.0.5...v2.1.0 ### Added * [#134](https://github.com/hashie/hashie/pull/134): Added deep_fetch extension for nested access - [@tylerdooling](https://github.com/tylerdooling). ### Changed * [#89](https://github.com/hashie/hashie/issues/89): Do not respond to every method with suffix in Hashie::Mash, fixes Rails strong_parameters - [@Maxim-Filimonov](https://github.com/Maxim-Filimonov). * Ruby style now enforced with Rubocop - [@dblock](https://github.com/dblock). ### Removed * Removed support for Ruby 1.8.7 - [@dblock](https://github.com/dblock). * [#136](https://github.com/hashie/hashie/issues/136): Removed Hashie::Extensions::Structure - [@markiz](https://github.com/markiz). ### Fixed * [#69](https://github.com/hashie/hashie/issues/69): Fixed assigning multiple properties in Hashie::Trash - [@einzige](https://github.com/einzige). * [#99](https://github.com/hashie/hashie/issues/99): Hash#deep_merge raises errors when it encounters integers - [@defsprite](https://github.com/defsprite). * [#100](https://github.com/hashie/hashie/pull/100): IndifferentAccess#store will respect indifference - [@jrochkind](https://github.com/jrochkind). * [#103](https://github.com/hashie/hashie/pull/103): Fixed support for Hashie::Dash properties that end in bang - [@thedavemarshall](https://github.com/thedavemarshall). * [#107](https://github.com/hashie/hashie/pull/107): Fixed excessive value conversions, poor performance of deep merge in Hashie::Mash - [@davemitchell](https://github.com/dblock), [@dblock](https://github.com/dblock). * [#110](https://github.com/hashie/hashie/pull/110): Correctly use Hash#default from Mash#method_missing - [@ryansouza](https://github.com/ryansouza). * [#111](https://github.com/hashie/hashie/issues/111): Trash#translations correctly maps original to translated names - [@artm](https://github.com/artm). * [#113](https://github.com/hashie/hashie/issues/113): Fixed Hash#merge with Hashie::Dash - [@spencer1248](https://github.com/spencer1248). * [#120](https://github.com/hashie/hashie/pull/120): Pass options to recursive to_hash calls - [@pwillett](https://github.com/pwillett). * [#129](https://github.com/hashie/hashie/pull/129): Added Trash#permitted_input_keys and inverse_translations - [@artm](https://github.com/artm). * [#130](https://github.com/hashie/hashie/pull/130): IndifferentAccess now works without MergeInitializer - [@npj](https://github.com/npj). * [#133](https://github.com/hashie/hashie/pull/133): Fixed Hash##to_hash with symbolize_keys - [@mhuggins](https://github.com/mhuggins). ## [2.0.5] - 2013-05-10 [2.0.5]: https://github.com/hashie/hashie/compare/v2.0.4...v2.0.5 ### Fixed * [#96](https://github.com/hashie/hashie/pull/96): Made coercion work better with non-symbol keys in Hashie::Mash - [@wapcaplet](https://github.com/wapcaplet). ## [2.0.4] - 2013-04-24 [2.0.4]: https://github.com/hashie/hashie/compare/v2.0.3...v2.0.4 ### Fixed * [#94](https://github.com/hashie/hashie/pull/94): Made #fetch method consistent with normal Hash - [@markiz](https://github.com/markiz). ### Changed * [#90](https://github.com/hashie/hashie/pull/90): Various doc tweaks - [@craiglittle](https://github.com/craiglittle). ## [2.0.3] - 2013-03-18 [2.0.3]: https://github.com/hashie/hashie/compare/v2.0.2...v2.0.3 ### Fixed * [#68](https://github.com/hashie/hashie/pull/68): Fixed #replace - [@jimeh](https://github.com/jimeh). * [#88](https://github.com/hashie/hashie/pull/88): Hashie::Mash.new(abc: true).respond_to?(:abc?) works - [@7even](https://github.com/7even). ## [2.0.2] - 2013-02-26 [2.0.2]: https://github.com/hashie/hashie/compare/v2.0.1...v2.0.2 ### Fixed * [#85](https://github.com/hashie/hashie/pull/85): Added symbolize_keys back to to_hash - [@cromulus](https://github.com/cromulus). ## [2.0.1] - 2013-02-26 [2.0.1]: https://github.com/hashie/hashie/compare/v2.0.0...v2.0.1 ### Removed * [#81](https://github.com/hashie/hashie/pull/81): Removed Mash#object_id override - [@matschaffer](https://github.com/matschaffer). * Removed VERSION and Gemfile.lock - [@jch](https://github.com/jch), [@mbleigh](https://github.com/mbleigh). ## [2.0.0] - 2013-02-16 [2.0.0]: https://github.com/hashie/hashie/compare/v1.2.0...v2.0.0 ### Added * [#41](https://github.com/hashie/hashie/pull/41): DeepMerge extension - [@nashby](https://github.com/nashby). * [#78](https://github.com/hashie/hashie/pull/78): Merge and update accepts a block - [@jch](https://github.com/jch). * [#72](https://github.com/hashie/hashie/pull/72): Updated gemspec with license info - [@jordimassaguerpla](https://github.com/jordimassaguerpla). ### Changed * [#28](https://github.com/hashie/hashie/pull/28): Hashie::Extensions::Coercion coerce_keys takes arguments - [@mattfawcett](https://github.com/mattfawcett). * [#77](https://github.com/hashie/hashie/pull/77): Removed id, type, and object_id as special allowable keys - [@jch](https://github.com/jch). ### Fixed * [#27](https://github.com/hashie/hashie/pull/27): Initialize with merge coerces values - [@mattfawcett](https://github.com/mattfawcett). * [#39](https://github.com/hashie/hashie/pull/39): Trash removes translated values on initialization - [@sleverbor](https://github.com/sleverbor). * [#49](https://github.com/hashie/hashie/pull/49): Hashie::Hash inherits from ::Hash to avoid ambiguity - [@meh](https://github.com/meh), [@orend](https://github.com/orend). * [#62](https://github.com/hashie/hashie/pull/62): Updated respond_to? method signature to match ruby core definition - [@dlupu](https://github.com/dlupu). * [#63](https://github.com/hashie/hashie/pull/63): Dash defaults are dup'ed before assigned - [@ohrite](https://github.com/ohrite). * [#66](https://github.com/hashie/hashie/pull/66): Mash#fetch works with symbol or string keys - [@arthwood](https://github.com/arthwood). hashie-5.0.0/CONTRIBUTING.md000066400000000000000000000116641414227716500152270ustar00rootroot00000000000000Contributing to Hashie ====================== Hashie is work of [many contributors](https://github.com/hashie/hashie/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/hashie/hashie/pulls), [propose features and discuss issues](https://github.com/hashie/hashie/issues). #### Fork the Project Fork the [project on Github](https://github.com/hashie/hashie) and check out your copy. ``` git clone https://github.com/contributor/hashie.git cd hashie git remote add upstream https://github.com/hashie/hashie.git ``` #### Create a Topic Branch Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. ``` git checkout master git pull upstream master git checkout -b my-feature-branch ``` #### Install dependencies You can use the setup script to install dependencies for the gem and its integration tests. ``` bin/setup ``` #### Test Ensure that you can build the project and run tests. ``` bundle exec rake ``` #### Write Tests Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [spec/hashie](spec/hashie). We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. #### Write Code Implement your feature or bug fix. Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop), run `bundle exec rubocop` and fix any style issues highlighted. Make sure that `bundle exec rake` completes without errors. #### Write Documentation Document any external behavior in the [README](README.md). #### Update Changelog Add a line to [CHANGELOG](CHANGELOG.md) under *Unreleased*. Make it look like every other line, including your name and link to your Github account. There are several categorizations of changes that you can choose from. Add your line to the appropriate section, following these conventions: * **Added** - When you add a new behavior to any class or module (or add a new extension) that does not break backwards compatibility, you should mark it as "added". This is generally a fully new behavior that does not touch any pre-existing public API. Changes here require a MINOR version bump, following the Semantic Versioning specification. * **Changed** - You should mark any change to the behavior of a public API on any class or module as "changed". Changes here require a MAJOR version bump, following the Semantic Versioning specification. * **Deprecated** - Any time you deprecate part of the public API on any class or module you should mark the change as "deprecated". Deprecated behavior will be removed in the next MAJOR version bump, but should be left in until then. Changes here require a MINOR version bump, following the Semantic Versioning specification. * **Removed** - You should mark any behavior that you removed from a public API on any class or module as "removed". Changes here require a MAJOR version bump, following the Semantic Versioning specification. * **Fixed** - Any time you fix a bug you should mark as "fixed". Changes here require a PATCH version bump. * **Security** - You should mark any security issue that you fix as "security". Changes here require a PATCH version bump. * **Miscellaneous** - Mark any other changes you make (i.e. documentation updates, test harness changes, etc.) as "miscellaneous". Changes here require a PATCH version bump. #### Commit Changes Make sure git knows your name and email address: ``` git config --global user.name "Your Name" git config --global user.email "contributor@example.com" ``` Writing good commit logs is important. A commit log should describe what changed and why. ``` git add ... git commit ``` #### Push ``` git push origin my-feature-branch ``` #### Make a Pull Request Go to https://github.com/contributor/hashie and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. #### Rebase If you've been working on a change for a while, rebase with upstream/master. ``` git fetch upstream git rebase upstream/master git push origin my-feature-branch -f ``` #### Update CHANGELOG Again Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows. ``` * [#123](https://github.com/hashie/hashie/pull/123): Reticulated splines - [@contributor](https://github.com/contributor). ``` Amend your previous commit and force push the changes. ``` git commit --amend git push origin my-feature-branch -f ``` #### Check on Your Pull Request Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. #### Be Patient It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! #### Thank You Please do know that we really appreciate and value your time and work. We love you, really. hashie-5.0.0/Dangerfile000066400000000000000000000001371414227716500147520ustar00rootroot00000000000000# see http://danger.systems changelog.format = :keep_a_changelog changelog.check! toc.check! hashie-5.0.0/Gemfile000066400000000000000000000020511414227716500142570ustar00rootroot00000000000000source 'http://rubygems.org' gemspec group :development do gem 'benchmark-ips' gem 'benchmark-memory' gem 'guard', '~> 2.6.1' gem 'guard-rspec', '~> 4.3.1', require: false gem 'guard-yield', '~> 0.1.0', require: false gem 'pry' gem 'pry-stack_explorer', platforms: %i[ruby_19 ruby_20 ruby_21] gem 'rubocop', '0.52.1' group :test do # ActiveSupport required to test compatibility with ActiveSupport Core Extensions. # rubocop:disable Bundler/DuplicatedGem require File.expand_path('../lib/hashie/extensions/ruby_version', __FILE__) if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >= Hashie::Extensions::RubyVersion.new('2.4.0') gem 'activesupport', '~> 5.x', require: false else gem 'activesupport', '~> 4.x', require: false end # rubocop:enable Bundler/DuplicatedGem gem 'rake' gem 'rspec', '~> 3' gem 'rspec-pending_for', '~> 0.1' end end group :test do gem 'danger-changelog', '~> 0.6.1', require: false gem 'danger-toc', '~> 0.2.0', require: false gem 'simplecov' end hashie-5.0.0/Guardfile000066400000000000000000000016141414227716500146150ustar00rootroot00000000000000require_relative 'spec/support/integration_specs' run_all = lambda do |*| Compat::UI.info('Running all integration tests', reset: true) run_all_integration_specs(logger: ->(msg) { Compat::UI.info(msg) }) end guard 'rspec', all_on_start: false, cmd: 'bundle exec rspec', run_all: { cmd: 'bundle exec rspec --exclude-pattern "spec/integration/**/*_spec.rb"' } do watch(%r{^spec(?!/integration)/.+_spec\.rb}) watch(%r{^lib/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { 'spec' } end guard :yield, run_all: run_all do watch(%r{^lib/(.+)\.rb}) { run_all.call } watch(%r{^spec/integration/(?.*)/.+_spec\.rb}) do |file, integration| Compat::UI.info(%(Running "#{integration}" integration test), reset: true) system(integration_command(integration, 'bundle --quiet')) system(integration_command(integration, "bundle exec rspec #{file}")) end end hashie-5.0.0/LICENSE000066400000000000000000000020721414227716500137740ustar00rootroot00000000000000Copyright (c) 2009-2020 Intridea, Inc., and Contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. hashie-5.0.0/README.md000066400000000000000000001110051414227716500142430ustar00rootroot00000000000000# Hashie [![Join the chat at https://gitter.im/hashie/hashie](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hashie/hashie?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Gem Version](http://img.shields.io/gem/v/hashie.svg)](http://badge.fury.io/rb/hashie) [![Build Status](https://github.com/hashie/hashie/actions/workflows/main.yml/badge.svg)](https://github.com/hashie/hashie/actions/workflows/main.yml) [![eierlegende Wollmilchsau](./mascot.svg)](#mascot) Hashie is a growing collection of tools that extend Hashes and make them more useful. # Table of Contents - [Hashie](#hashie) - [Table of Contents](#table-of-contents) - [Installation](#installation) - [Stable Release](#stable-release) - [Hash Extensions](#hash-extensions) - [Logging](#logging) - [Coercion](#coercion) - [Coercing Collections](#coercing-collections) - [Coercing Hashes](#coercing-hashes) - [Coercing Core Types](#coercing-core-types) - [Coercion Proc](#coercion-proc) - [A note on circular coercion](#a-note-on-circular-coercion) - [KeyConversion](#keyconversion) - [MergeInitializer](#mergeinitializer) - [MethodAccess](#methodaccess) - [MethodAccessWithOverride](#methodaccesswithoverride) - [MethodOverridingInitializer](#methodoverridinginitializer) - [IndifferentAccess](#indifferentaccess) - [IgnoreUndeclared](#ignoreundeclared) - [DeepMerge](#deepmerge) - [DeepFetch](#deepfetch) - [DeepFind](#deepfind) - [DeepLocate](#deeplocate) - [StrictKeyAccess](#strictkeyaccess) - [Mash](#mash) - [KeepOriginalKeys](#keeporiginalkeys) - [PermissiveRespondTo](#permissiverespondto) - [SafeAssignment](#safeassignment) - [SymbolizeKeys](#symbolizekeys) - [DefineAccessors](#defineaccessors) - [Dash](#dash) - [Potential Gotchas](#potential-gotchas) - [PropertyTranslation](#propertytranslation) - [Mash and Rails 4 Strong Parameters](#mash-and-rails-4-strong-parameters) - [Coercion](#coercion-1) - [PredefinedValues](#predefinedvalues) - [Trash](#trash) - [Clash](#clash) - [Rash](#rash) - [Auto-Optimized](#auto-optimized) - [Mascot](#mascot) - [Contributing](#contributing) - [Copyright](#copyright) ## Installation Hashie is available as a RubyGem: ```bash $ gem install hashie ``` ## Stable Release You're reading the documentation for the stable release of Hashie, v5.0.0. ## Hash Extensions The library is broken up into a number of atomically includable Hash extension modules as described below. This provides maximum flexibility for users to mix and match functionality while maintaining feature parity with earlier versions of Hashie. Any of the extensions listed below can be mixed into a class by `include`-ing `Hashie::Extensions::ExtensionName`. ## Logging Hashie has a built-in logger that you can override. By default, it logs to `STDOUT` but can be replaced by any `Logger` class. The logger is accessible on the Hashie module, as shown below: ```ruby # Set the logger to the Rails logger Hashie.logger = Rails.logger ``` ### Coercion Coercions allow you to set up "coercion rules" based either on the key or the value type to massage data as it's being inserted into the Hash. Key coercions might be used, for example, in lightweight data modeling applications such as an API client: ```ruby class Tweet < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer coerce_key :user, User end user_hash = { name: "Bob" } Tweet.new(user: user_hash) # => automatically calls User.coerce(user_hash) or # User.new(user_hash) if that isn't present. ``` Value coercions, on the other hand, will coerce values based on the type of the value being inserted. This is useful if you are trying to build a Hash-like class that is self-propagating. ```ruby class SpecialHash < Hash include Hashie::Extensions::Coercion coerce_value Hash, SpecialHash def initialize(hash = {}) super hash.each_pair do |k,v| self[k] = v end end end ``` ### Coercing Collections ```ruby class Tweet < Hash include Hashie::Extensions::Coercion coerce_key :mentions, Array[User] coerce_key :friends, Set[User] end user_hash = { name: "Bob" } mentions_hash= [user_hash, user_hash] friends_hash = [user_hash] tweet = Tweet.new(mentions: mentions_hash, friends: friends_hash) # => automatically calls User.coerce(user_hash) or # User.new(user_hash) if that isn't present on each element of the array tweet.mentions.map(&:class) # => [User, User] tweet.friends.class # => Set ``` ### Coercing Hashes ```ruby class Relation def initialize(string) @relation = string end end class Tweet < Hash include Hashie::Extensions::Coercion coerce_key :relations, Hash[User => Relation] end user_hash = { name: "Bob" } relations_hash= { user_hash => "father", user_hash => "friend" } tweet = Tweet.new(relations: relations_hash) tweet.relations.map { |k,v| [k.class, v.class] } # => [[User, Relation], [User, Relation]] tweet.relations.class # => Hash # => automatically calls User.coerce(user_hash) on each key # and Relation.new on each value since Relation doesn't define the `coerce` class method ``` ### Coercing Core Types Hashie handles coercion to the following by using standard conversion methods: | type | method | |----------|----------| | Integer | `#to_i` | | Float | `#to_f` | | Complex | `#to_c` | | Rational | `#to_r` | | String | `#to_s` | | Symbol | `#to_sym`| **Note**: The standard Ruby conversion methods are less strict than you may assume. For example, `:foo.to_i` raises an error but `"foo".to_i` returns 0. You can also use coerce from the following supertypes with `coerce_value`: - Integer - Numeric Hashie does not have built-in support for coercing boolean values, since Ruby does not have a built-in boolean type or standard method for coercing to a boolean. You can coerce to booleans using a custom proc. ### Coercion Proc You can use a custom coercion proc on either `#coerce_key` or `#coerce_value`. This is useful for coercing to booleans or other simple types without creating a new class and `coerce` method. For example: ```ruby class Tweet < Hash include Hashie::Extensions::Coercion coerce_key :retweeted, ->(v) do case v when String !!(v =~ /\A(true|t|yes|y|1)\z/i) when Numeric !v.to_i.zero? else v == true end end end ``` #### A note on circular coercion Since `coerce_key` is a class-level method, you cannot have circular coercion without the use of a proc. For example: ```ruby class CategoryHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer coerce_key :products, Array[ProductHash] end class ProductHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer coerce_key :categories, Array[CategoriesHash] end ``` This will fail with a `NameError` for `CategoryHash::ProductHash` because `ProductHash` is not defined at the point that `coerce_key` is happening for `CategoryHash`. To work around this, you can use a coercion proc. For example, you could do: ```ruby class CategoryHash < Hash # ... coerce_key :products, ->(value) do return value.map { |v| ProductHash.new(v) } if value.respond_to?(:map) ProductHash.new(value) end end ``` ### KeyConversion The KeyConversion extension gives you the convenience methods of `symbolize_keys` and `stringify_keys` along with their bang counterparts. You can also include just stringify or just symbolize with `Hashie::Extensions::StringifyKeys` or `Hashie::Extensions::SymbolizeKeys`. Hashie also has a utility method for converting keys on a Hash without a mixin: ```ruby Hashie.symbolize_keys! hash # => Symbolizes all string keys of hash. Hashie.symbolize_keys hash # => Returns a copy of hash with string keys symbolized. Hashie.stringify_keys! hash # => Stringifies keys of hash. Hashie.stringify_keys hash # => Returns a copy of hash with keys stringified. ``` ### MergeInitializer The MergeInitializer extension simply makes it possible to initialize a Hash subclass with another Hash, giving you a quick short-hand. ### MethodAccess The MethodAccess extension allows you to quickly build method-based reading, writing, and querying into your Hash descendant. It can also be included as individual modules, i.e. `Hashie::Extensions::MethodReader`, `Hashie::Extensions::MethodWriter` and `Hashie::Extensions::MethodQuery`. ```ruby class MyHash < Hash include Hashie::Extensions::MethodAccess end h = MyHash.new h.abc = 'def' h.abc # => 'def' h.abc? # => true ``` ### MethodAccessWithOverride The MethodAccessWithOverride extension is like the MethodAccess extension, except that it allows you to override Hash methods. It aliases any overridden method with two leading underscores. To include only this overriding functionality, you can include the single module `Hashie::Extensions::MethodOverridingWriter`. ```ruby class MyHash < Hash include Hashie::Extensions::MethodAccess end class MyOverridingHash < Hash include Hashie::Extensions::MethodAccessWithOverride end non_overriding = MyHash.new non_overriding.zip = 'a-dee-doo-dah' non_overriding.zip #=> [[['zip', 'a-dee-doo-dah']]] overriding = MyOverridingHash.new overriding.zip = 'a-dee-doo-dah' overriding.zip #=> 'a-dee-doo-dah' overriding.__zip #=> [[['zip', 'a-dee-doo-dah']]] ``` ### MethodOverridingInitializer The MethodOverridingInitializer extension will override hash methods if you pass in a normal hash to the constructor. It aliases any overridden method with two leading underscores. To include only this initializing functionality, you can include the single module `Hashie::Extensions::MethodOverridingInitializer`. ```ruby class MyHash < Hash end class MyOverridingHash < Hash include Hashie::Extensions::MethodOverridingInitializer end non_overriding = MyHash.new(zip: 'a-dee-doo-dah') non_overriding.zip #=> [] overriding = MyOverridingHash.new(zip: 'a-dee-doo-dah') overriding.zip #=> 'a-dee-doo-dah' overriding.__zip #=> [[['zip', 'a-dee-doo-dah']]] ``` ### IndifferentAccess This extension can be mixed in to your Hash subclass to allow you to use Strings or Symbols interchangeably as keys; similar to the `params` hash in Rails. In addition, IndifferentAccess will also inject itself into sub-hashes so they behave the same. ```ruby class MyHash < Hash include Hashie::Extensions::MergeInitializer include Hashie::Extensions::IndifferentAccess end myhash = MyHash.new(:cat => 'meow', 'dog' => 'woof') myhash['cat'] # => "meow" myhash[:cat] # => "meow" myhash[:dog] # => "woof" myhash['dog'] # => "woof" # Auto-Injecting into sub-hashes. myhash['fishes'] = {} myhash['fishes'].class # => Hash myhash['fishes'][:food] = 'flakes' myhash['fishes']['food'] # => "flakes" ``` To get back a normal, not-indifferent Hash, you can use `#to_hash` on the indifferent hash. It exports the keys as strings, not symbols: ```ruby myhash = MyHash.new myhash["foo"] = "bar" myhash[:foo] #=> "bar" normal_hash = myhash.to_hash myhash["foo"] #=> "bar" myhash[:foo] #=> nil ``` ### IgnoreUndeclared This extension can be mixed in to silently ignore undeclared properties on initialization instead of raising an error. This is useful when using a Trash to capture a subset of a larger hash. ```ruby class Person < Trash include Hashie::Extensions::IgnoreUndeclared property :first_name property :last_name end user_data = { first_name: 'Freddy', last_name: 'Nostrils', email: 'freddy@example.com' } p = Person.new(user_data) # 'email' is silently ignored p.first_name # => 'Freddy' p.last_name # => 'Nostrils' p.email # => NoMethodError ``` ### DeepMerge This extension allows you to easily include a recursive merging system into any Hash descendant: ```ruby class MyHash < Hash include Hashie::Extensions::DeepMerge end h1 = MyHash[{ x: { y: [4,5,6] }, z: [7,8,9] }] h2 = MyHash[{ x: { y: [7,8,9] }, z: "xyz" }] h1.deep_merge(h2) # => { x: { y: [7, 8, 9] }, z: "xyz" } h2.deep_merge(h1) # => { x: { y: [4, 5, 6] }, z: [7, 8, 9] } ``` Like with Hash#merge in the standard library, a block can be provided to merge values: ```ruby class MyHash < Hash include Hashie::Extensions::DeepMerge end h1 = MyHash[{ a: 100, b: 200, c: { c1: 100 } }] h2 = MyHash[{ b: 250, c: { c1: 200 } }] h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val } # => { a: 100, b: 450, c: { c1: 300 } } ``` ### DeepFetch This extension can be mixed in to provide for safe and concise retrieval of deeply nested hash values. In the event that the requested key does not exist a block can be provided and its value will be returned. Though this is a hash extension, it conveniently allows for arrays to be present in the nested structure. This feature makes the extension particularly useful for working with JSON API responses. ```ruby user = { name: { first: 'Bob', last: 'Boberts' }, groups: [ { name: 'Rubyists' }, { name: 'Open source enthusiasts' } ] } user.extend Hashie::Extensions::DeepFetch user.deep_fetch :name, :first # => 'Bob' user.deep_fetch :name, :middle # => 'KeyError: Could not fetch middle' # using a default block user.deep_fetch(:name, :middle) { |key| 'default' } # => 'default' # a nested array user.deep_fetch :groups, 1, :name # => 'Open source enthusiasts' ``` ### DeepFind This extension can be mixed in to provide for concise searching for keys within a deeply nested hash. It can also search through any Enumerable contained within the hash for objects with the specified key. Note: The searches are depth-first, so it is not guaranteed that a shallowly nested value will be found before a deeply nested value. ```ruby user = { name: { first: 'Bob', last: 'Boberts' }, groups: [ { name: 'Rubyists' }, { name: 'Open source enthusiasts' } ] } user.extend Hashie::Extensions::DeepFind user.deep_find(:name) #=> { first: 'Bob', last: 'Boberts' } user.deep_detect(:name) #=> { first: 'Bob', last: 'Boberts' } user.deep_find_all(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'Open source enthusiasts'] user.deep_select(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'Open source enthusiasts'] ``` ### DeepLocate This extension can be mixed in to provide a depth first search based search for enumerables matching a given comparator callable. It returns all enumerables which contain at least one element, for which the given comparator returns ```true```. Because the container objects are returned, the result elements can be modified in place. This way, one can perform modifications on deeply nested hashes without the need to know the exact paths. ```ruby books = [ { title: "Ruby for beginners", pages: 120 }, { title: "CSS for intermediates", pages: 80 }, { title: "Collection of ruby books", books: [ { title: "Ruby for the rest of us", pages: 576 } ] } ] books.extend(Hashie::Extensions::DeepLocate) # for ruby 1.9 leave *no* space between the lambda rocket and the braces # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/ books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") } # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"Ruby for the rest of us", :pages=>576}] books.deep_locate -> (key, value, object) { key == :pages && value <= 120 } # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}] ``` ## StrictKeyAccess This extension can be mixed in to allow a Hash to raise an error when attempting to extract a value using a non-existent key. ```ruby class StrictKeyAccessHash < Hash include Hashie::Extensions::StrictKeyAccess end >> hash = StrictKeyAccessHash[foo: "bar"] => {:foo=>"bar"} >> hash[:foo] => "bar" >> hash[:cow] KeyError: key not found: :cow ``` ## Mash Mash is an extended Hash that gives simple pseudo-object functionality that can be built from hashes and easily extended. It is intended to give the user easier access to the objects within the Mash through a property-like syntax, while still retaining all Hash functionality. ```ruby mash = Hashie::Mash.new mash.name? # => false mash.name # => nil mash.name = "My Mash" mash.name # => "My Mash" mash.name? # => true mash.inspect # => mash = Hashie::Mash.new # use bang methods for multi-level assignment mash.author!.name = "Michael Bleigh" mash.author # => mash = Hashie::Mash.new # use under-bang methods for multi-level testing mash.author_.name? # => false mash.inspect # => ``` **Note:** The `?` method will return false if a key has been set to false or nil. In order to check if a key has been set at all, use the `mash.key?('some_key')` method instead. _How does Mash handle conflicts with pre-existing methods?_ Please note that a Mash will not override methods through the use of the property-like syntax. This can lead to confusion if you expect to be able to access a Mash value through the property-like syntax for a key that conflicts with a method name. However, it protects users of your library from the unexpected behavior of those methods being overridden behind the scenes. ```ruby mash = Hashie::Mash.new mash.name = "My Mash" mash.zip = "Method Override?" mash.zip # => [[["name", "My Mash"]], [["zip", "Method Override?"]]] ``` Since Mash gives you the ability to set arbitrary keys that then act as methods, Hashie logs when there is a conflict between a key and a pre-existing method. You can set the logger that this logs message to via the global Hashie logger: ```ruby Hashie.logger = Rails.logger ``` You can also disable the logging in subclasses of Mash: ```ruby class Response < Hashie::Mash disable_warnings end ``` The default is to disable logging for all methods that conflict. If you would like to only disable the logging for specific methods, you can include an array of method keys: ```ruby class Response < Hashie::Mash disable_warnings :zip, :zap end ``` This behavior is cumulative. The examples above and below behave identically. ```ruby class Response < Hashie::Mash disable_warnings :zip disable_warnings :zap end ``` Disable warnings will honor the last `disable_warnings` call. Calling without parameters will override the ignored methods list, and calling with parameters will create a new ignored methods list. This includes child classes that inherit from a class that disables warnings. ```ruby class Message < Hashie::Mash disable_warnings :zip, :zap disable_warnings end # No errors will be logged Message.new(merge: 'true', compact: true) ``` ```ruby class Message < Hashie::Mash disable_warnings end class Response < Message disable_warnings :zip, :zap end # 2 errors will be logged Response.new(merge: 'true', compact: true, zip: '90210', zap: 'electric') ``` If you would like to create an anonymous subclass of a Hashie::Mash with key conflict warnings disabled: ```ruby Hashie::Mash.quiet.new(zip: '90210', compact: true) # no errors logged Hashie::Mash.quiet(:zip).new(zip: '90210', compact: true) # error logged for compact ``` _How does the wrapping of Mash sub-Hashes work?_ Mash duplicates any sub-Hashes that you add to it and wraps them in a Mash. This allows for infinite chaining of nested Hashes within a Mash without modifying the object(s) that are passed into the Mash. When you subclass Mash, the subclass wraps any sub-Hashes in its own class. This preserves any extensions that you mixed into the Mash subclass and allows them to work within the sub-Hashes, in addition to the main containing Mash. ```ruby mash = Hashie::Mash.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0" }) mash.dependencies.class #=> Hashie::Mash class MyGem < Hashie::Mash; end my_gem = MyGem.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0" }) my_gem.dependencies.class #=> MyGem ``` _How does Mash handle key types which cannot be symbolized?_ Mash preserves keys which cannot be converted *directly* to both a string and a symbol, such as numeric keys. Since Mash is conceived to provide psuedo-object functionality, handling keys which cannot represent a method call falls outside its scope of value. ```ruby Hashie::Mash.new('1' => 'one string', :'1' => 'one sym', 1 => 'one num') # => {"1"=>"one sym", 1=>"one num"} ``` The symbol key `:'1'` is converted the string `'1'` to support indifferent access and consequently its value `'one sym'` will override the previously set `'one string'`. However, the subsequent key of `1` cannot directly convert to a symbol and therefore **not** converted to the string `'1'` that would otherwise override the previously set value of `'one sym'`. _What else can Mash do?_ Mash allows you also to transform any files into a Mash objects. ```yml #/etc/config/settings/twitter.yml development: api_key: 'api_key' production: api_key: <%= ENV['API_KEY'] %> #let's say that ENV['API_KEY'] is set to 'abcd' ``` ```ruby mash = Mash.load('settings/twitter.yml') mash.development.api_key # => 'localhost' mash.development.api_key = "foo" # => <# RuntimeError can't modify frozen ...> mash.development.api_key? # => true ``` You can also load with a `Pathname` object: ```ruby mash = Mash.load(Pathname 'settings/twitter.yml') mash.development.api_key # => 'localhost' ``` You can access a Mash from another class: ```ruby mash = Mash.load('settings/twitter.yml')[ENV['RACK_ENV']] Twitter.extend mash.to_module # NOTE: if you want another name than settings, call: to_module('my_settings') Twitter.settings.api_key # => 'abcd' ``` You can use another parser (by default: [YamlErbParser](lib/hashie/extensions/parsers/yaml_erb_parser.rb)): ``` #/etc/data/user.csv id | name | lastname ---|------------- | ------------- 1 |John | Doe 2 |Laurent | Garnier ``` ```ruby mash = Mash.load('data/user.csv', parser: MyCustomCsvParser) # => { 1 => { name: 'John', lastname: 'Doe'}, 2 => { name: 'Laurent', lastname: 'Garnier' } } mash[1] #=> { name: 'John', lastname: 'Doe' } ``` The `Mash#load` method calls `YAML.safe_load(path, [], [], true)`. Specify `permitted_symbols`, `permitted_classes` and `aliases` options as needed. ```ruby Mash.load('data/user.csv', permitted_classes: [Symbol], permitted_symbols: [], aliases: false) ``` ### KeepOriginalKeys This extension can be mixed into a Mash to keep the form of any keys passed directly into the Mash. By default, Mash converts symbol keys to strings to give indifferent access. This extension still allows indifferent access, but keeps the form of the keys to eliminate confusion when you're not expecting the keys to change. ```ruby class KeepingMash < ::Hashie::Mash include Hashie::Extensions::Mash::KeepOriginalKeys end mash = KeepingMash.new(:symbol_key => :symbol, 'string_key' => 'string') mash.to_hash == { :symbol_key => :symbol, 'string_key' => 'string' } #=> true mash.symbol_key #=> :symbol mash[:symbol_key] #=> :symbol mash['symbol_key'] #=> :symbol mash.string_key #=> 'string' mash['string_key'] #=> 'string' mash[:string_key] #=> 'string' ``` ### PermissiveRespondTo By default, Mash only states that it responds to built-in methods, affixed methods (e.g. setters, underbangs, etc.), and keys that it currently contains. That means it won't state that it responds to a getter for an unset key, as in the following example: ```ruby mash = Hashie::Mash.new(a: 1) mash.respond_to? :b #=> false ``` This means that by default Mash is not a perfect match for use with a SimpleDelegator since the delegator will not forward messages for unset keys to the Mash even though it can handle them. In order to have a SimpleDelegator-compatible Mash, you can use the `PermissiveRespondTo` extension to make Mash respond to anything. ```ruby class PermissiveMash < Hashie::Mash include Hashie::Extensions::Mash::PermissiveRespondTo end mash = PermissiveMash.new(a: 1) mash.respond_to? :b #=> true ``` This comes at the cost of approximately 20% performance for initialization and setters and 19KB of permanent memory growth for each such class that you create. ### SafeAssignment This extension can be mixed into a Mash to guard the attempted overwriting of methods by property setters. When mixed in, the Mash will raise an `ArgumentError` if you attempt to write a property with the same name as an existing method. ```ruby class SafeMash < ::Hashie::Mash include Hashie::Extensions::Mash::SafeAssignment end safe_mash = SafeMash.new safe_mash.zip = 'Test' # => ArgumentError safe_mash[:zip] = 'test' # => still ArgumentError ``` ### SymbolizeKeys This extension can be mixed into a Mash to change the default behavior of converting keys to strings. After mixing this extension into a Mash, the Mash will convert all string keys to symbols. It can be useful to use with keywords argument, which required symbol keys. ```ruby class SymbolizedMash < ::Hashie::Mash include Hashie::Extensions::Mash::SymbolizeKeys end symbol_mash = SymbolizedMash.new symbol_mash['test'] = 'value' symbol_mash.test #=> 'value' symbol_mash.to_h #=> {test: 'value'} def example(test:) puts test end example(symbol_mash) #=> value ``` There is a major benefit and coupled with a major trade-off to this decision (at least on older Rubies). As a benefit, by using symbols as keys, you will be able to use the implicit conversion of a Mash via the `#to_hash` method to destructure (or splat) the contents of a Mash out to a block. This can be handy for doing iterations through the Mash's keys and values, as follows: ```ruby symbol_mash = SymbolizedMash.new(id: 123, name: 'Rey') symbol_mash.each do |key, value| # key is :id, then :name # value is 123, then 'Rey' end ``` However, on Rubies less than 2.0, this means that every key you send to the Mash will generate a symbol. Since symbols are not garbage-collected on older versions of Ruby, this can cause a slow memory leak when using a symbolized Mash with data generated from user input. ### DefineAccessors This extension can be mixed into a Mash so it makes it behave like `OpenStruct`. It reduces the overhead of `method_missing?` magic by lazily defining field accessors when they're requested. ```ruby class MyHash < ::Hashie::Mash include Hashie::Extensions::Mash::DefineAccessors end mash = MyHash.new MyHash.method_defined?(:foo=) #=> false mash.foo = 123 MyHash.method_defined?(:foo=) #=> true MyHash.method_defined?(:foo) #=> false mash.foo #=> 123 MyHash.method_defined?(:foo) #=> true ``` You can also extend the existing mash without defining a class: ```ruby mash = ::Hashie::Mash.new.with_accessors! ``` ## Dash Dash is an extended Hash that has a discrete set of defined properties and only those properties may be set on the hash. Additionally, you can set defaults for each property. You can also flag a property as required. Required properties will raise an exception if unset. Another option is message for required properties, which allow you to add custom messages for required property. You can also conditionally require certain properties by passing a Proc or Symbol. If a Proc is provided, it will be run in the context of the Dash instance. If a Symbol is provided, the value returned for the property or method of the same name will be evaluated. The property will be required if the result of the conditional is truthy. ```ruby class Person < Hashie::Dash property :name, required: true property :age, required: true, message: 'must be set.' property :email property :phone, required: -> { email.nil? }, message: 'is required if email is not set.' property :pants, required: :weekday?, message: 'are only required on weekdays.' property :occupation, default: 'Rubyist' def weekday? [ Time.now.saturday?, Time.now.sunday? ].none? end end p = Person.new # => ArgumentError: The property 'name' is required for this Dash. p = Person.new(name: 'Bob') # => ArgumentError: The property 'age' must be set. p = Person.new(name: "Bob", age: 18) p.name # => 'Bob' p.name = nil # => ArgumentError: The property 'name' is required for this Dash. p.age # => 18 p.age = nil # => ArgumentError: The property 'age' must be set. p.email = 'abc@def.com' p.occupation # => 'Rubyist' p.email # => 'abc@def.com' p[:awesome] # => NoMethodError p[:occupation] # => 'Rubyist' p.update_attributes!(name: 'Trudy', occupation: 'Evil') p.occupation # => 'Evil' p.name # => 'Trudy' p.update_attributes!(occupation: nil) p.occupation # => 'Rubyist' ``` Properties defined as symbols are not the same thing as properties defined as strings. ```ruby class Tricky < Hashie::Dash property :trick property 'trick' end p = Tricky.new(trick: 'one', 'trick' => 'two') p.trick # => 'one', always symbol version p[:trick] # => 'one' p['trick'] # => 'two' ``` Note that accessing a property as a method always uses the symbol version. ```ruby class Tricky < Hashie::Dash property 'trick' end p = Tricky.new('trick' => 'two') p.trick # => NoMethodError ``` If you would like to update a Dash and use any default values set in the case of a `nil` value, use `#update_attributes!`. ```ruby class WithDefaults < Hashie::Dash property :description, default: 'none' end dash = WithDefaults.new dash.description #=> 'none' dash.description = 'You committed one of the classic blunders!' dash.description #=> 'You committed one of the classic blunders!' dash.description = nil dash.description #=> nil dash.description = 'Only slightly less known is ...' dash.update_attributes!(description: nil) dash.description #=> 'none' ``` ### Potential Gotchas Because Dashes are subclasses of the built-in Ruby Hash class, the double-splat operator takes the Dash as-is without any conversion. This can lead to strange behavior when you use the double-splat operator on a Dash as the first part of a keyword list or built Hash. For example: ```ruby class Foo < Hashie::Dash property :bar end foo = Foo.new(bar: 'baz') #=> {:bar=>"baz"} qux = { **foo, quux: 'corge' } #=> {:bar=> "baz", :quux=>"corge"} qux.is_a?(Foo) #=> true qux[:quux] #=> raise NoMethodError, "The property 'quux' is not defined for Foo." qux.key?(:quux) #=> true ``` You can work around this problem in two ways: 1. Call `#to_h` on the resulting object to convert it into a Hash. 2. Use the double-splat operator on the Dash as the last argument in the Hash literal. This will cause the resulting object to be a Hash instead of a Dash, thereby circumventing the problem. ```ruby qux = { **foo, quux: 'corge' }.to_h #=> {:bar=> "baz", :quux=>"corge"} qux.is_a?(Hash) #=> true qux[:quux] #=> "corge" qux = { quux: 'corge', **foo } #=> {:quux=>"corge", :bar=> "baz"} qux.is_a?(Hash) #=> true qux[:quux] #=> "corge" ``` ### PropertyTranslation The `Hashie::Extensions::Dash::PropertyTranslation` mixin extends a Dash with the ability to remap keys from a source hash. Property translation is useful when you need to read data from another application -- such as a Java API -- where the keys are named differently from Ruby conventions. ```ruby class PersonHash < Hashie::Dash include Hashie::Extensions::Dash::PropertyTranslation property :first_name, from: :firstName property :last_name, from: :lastName property :first_name, from: :f_name property :last_name, from: :l_name end person = PersonHash.new(firstName: 'Michael', l_name: 'Bleigh') person[:first_name] #=> 'Michael' person[:last_name] #=> 'Bleigh ``` You can also use a lambda to translate the value. This is particularly useful when you want to ensure the type of data you're wrapping. ```ruby class DataModelHash < Hashie::Dash include Hashie::Extensions::Dash::PropertyTranslation property :id, transform_with: ->(value) { value.to_i } property :created_at, from: :created, with: ->(value) { Time.parse(value) } end model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28') model.id.class #=> Integer (Fixnum if you are using Ruby 2.3 or lower) model.created_at.class #=> Time ``` ### Mash and Rails 4 Strong Parameters To enable compatibility with Rails 4 use the [hashie-forbidden_attributes](https://github.com/Maxim-Filimonov/hashie-forbidden_attributes) gem. ### Coercion If you want to use `Hashie::Extensions::Coercion` together with `Dash` then you may probably want to use `Hashie::Extensions::Dash::Coercion` instead. This extension automatically includes `Hashie::Extensions::Coercion` and also adds a convenient `:coerce` option to `property` so you can define coercion in one line instead of using `property` and `coerce_key` separate: ```ruby class UserHash < Hashie::Dash include Hashie::Extensions::Coercion property :id property :posts coerce_key :posts, Array[PostHash] end ``` This is the same as: ```ruby class UserHash < Hashie::Dash include Hashie::Extensions::Dash::Coercion property :id property :posts, coerce: Array[PostHash] end ``` ### PredefinedValues The `Hashie::Extensions::Dash::PredefinedValues` mixin extends a Dash with the ability to accept predefined values on a property. ```ruby class UserHash < Hashie::Dash include Hashie::Extensions::Dash::PredefinedValues property :gender, values: %i[male female prefer_not_to_say] property :age, values: (0..150) end ``` ## Trash A Trash is a Dash that allows you to translate keys on initialization. It mixes in the PropertyTranslation mixin by default and is used like so: ```ruby class Person < Hashie::Trash property :first_name, from: :firstName end ``` This will automatically translate the firstName key to first_name when it is initialized using a hash such as through: ```ruby Person.new(firstName: 'Bob') ``` Trash also supports translations using lambda, this could be useful when dealing with external API's. You can use it in this way: ```ruby class Result < Hashie::Trash property :id, transform_with: lambda { |v| v.to_i } property :created_at, from: :creation_date, with: lambda { |v| Time.parse(v) } end ``` this will produce the following ```ruby result = Result.new(id: '123', creation_date: '2012-03-30 17:23:28') result.id.class # => Integer (Fixnum if you are using Ruby 2.3 or lower) result.created_at.class # => Time ``` ## Clash Clash is a Chainable Lazy Hash that allows you to easily construct complex hashes using method notation chaining. This will allow you to use a more action-oriented approach to building options hashes. Essentially, a Clash is a generalized way to provide much of the same kind of "chainability" that libraries like Arel or Rails 2.x's named_scopes provide. ```ruby c = Hashie::Clash.new c.where(abc: 'def').order(:created_at) c # => { where: { abc: 'def' }, order: :created_at } # You can also use bang notation to chain into sub-hashes, # jumping back up the chain with _end! c = Hashie::Clash.new c.where!.abc('def').ghi(123)._end!.order(:created_at) c # => { where: { abc: 'def', ghi: 123 }, order: :created_at } # Multiple hashes are merged automatically c = Hashie::Clash.new c.where(abc: 'def').where(hgi: 123) c # => { where: { abc: 'def', hgi: 123 } } ``` ## Rash Rash is a Hash whose keys can be Regexps or Ranges, which will map many input keys to a value. A good use case for the Rash is an URL router for a web framework, where URLs need to be mapped to actions; the Rash's keys match URL patterns, while the values call the action which handles the URL. If the Rash's value is a `proc`, the `proc` will be automatically called with the regexp's MatchData (matched groups) as a block argument. ```ruby # Mapping names to appropriate greetings greeting = Hashie::Rash.new( /^Mr./ => "Hello sir!", /^Mrs./ => "Evening, madame." ) greeting["Mr. Steve Austin"] # => "Hello sir!" greeting["Mrs. Steve Austin"] # => "Evening, madame." # Mapping statements to saucy retorts mapper = Hashie::Rash.new( /I like (.+)/ => proc { |m| "Who DOESN'T like #{m[1]}?!" }, /Get off my (.+)!/ => proc { |m| "Forget your #{m[1]}, old man!" } ) mapper["I like traffic lights"] # => "Who DOESN'T like traffic lights?!" mapper["Get off my lawn!"] # => "Forget your lawn, old man!" ``` ### Auto-Optimized **Note:** The Rash is automatically optimized every 500 accesses (which means that it sorts the list of Regexps, putting the most frequently matched ones at the beginning). If this value is too low or too high for your needs, you can tune it by setting: `rash.optimize_every = n`. ## Mascot [![eierlegende Wollmilchsau](./mascot.svg)](https://en.wiktionary.org/wiki/eierlegende_Wollmilchsau) Meet Hashie's "offical" mascot, the [eierlegende Wollmilchsau](https://en.wiktionary.org/wiki/eierlegende_Wollmilchsau)! ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) ## Copyright Copyright (c) 2009-2020 [Intridea, Inc.](http://intridea.com), and [contributors](https://github.com/hashie/hashie/graphs/contributors). MIT License. See [LICENSE](LICENSE) for details. hashie-5.0.0/RELEASING.md000066400000000000000000000071721414227716500146300ustar00rootroot00000000000000# Releasing Hashie There're no particular rules about when to release Hashie. Release bug fixes frequenty, features not so frequently and breaking API changes rarely. ## Release Run tests, check that all tests succeed locally. ```sh bundle install bundle exec rake ``` Check that the last build succeeded in [Travis CI](https://travis-ci.org/hashie/hashie) for all supported platforms. ### Check Next Version Increment the version, modify [lib/hashie/version.rb](lib/hashie/version.rb). [Changelog](CHANGELOG.md) entries should be helpfully categorized to assist in picking the next version number. * Increment the third number (minor version) if the release has bug fixes and/or very minor features, only (eg. change `0.5.1` to `0.5.2`). These should be in the "Fixed", "Security", or "Miscellaneous" categories in the change log. * Increment the second number (patch version) if the release contains major features or breaking API changes (eg. change `0.5.1` to `0.6.0`). These should be in the "Added" or "Deprecated" categories in the change log. * Increment the first number (major version) if the release has any changed or removed behavior on public APIs (eg. change `0.5.1` to `1.0.0`). These should be in the "Changed" or "Removed" categories in the change log. ### Modify the Readme Modify the "Stable Release" section in [README.md](README.md). Change the text to reflect that this is going to be the documentation for a stable release. Remove references to the previous release of Hashie. Keep the file open, you'll have to undo this change after the release. ```markdown ## Stable Release You're reading the documentation for the stable release of Hashie, 3.3.0. ``` ### Modify the Changelog Change "Unreleased" in [CHANGELOG.md](CHANGELOG.md) to the new version. ```markdown ## [3.3.0] - 2014-08-25 [3.3.0]: https://github.com/hashie/hashie/compare/v..v ``` Replace `` and `` with the last and new-to-be-released versions to set up the compare view on Github. Remove any sections that only have "Your contribution here." underneath them. Commit your changes. ```sh git add README.md CHANGELOG.md lib/hashie/version.rb git commit -m "Preparing for release, 3.3.0." git push origin master ``` ### Push to RubyGems.org Release. ```sh $ rake release hashie 3.3.0 built to pkg/hashie-3.3.0.gem. Tagged v3.3.0. Pushed git commits and tags. Pushed hashie 3.3.0 to rubygems.org. ``` ## Prepare for the Next Version Modify the "Stable Release" section in [README.md](README.md). Change the text to reflect that this is going to be the next release. ```markdown ## Stable Release You're reading the documentation for the next release of Hashie, which should be 3.3.1. The current stable release is [3.3.0](https://github.com/hashie/hashie/blob/v3.3.0/README.md). ``` Add new "Unreleased" section to [CHANGELOG.md](CHANGELOG.md) using this template: ```markdown ## [Unreleased][unreleased] [unreleased]: https://github.com/hashie/hashie/compare/v...master ### Added * Your contribution here. ### Changed * Your contribution here. ### Deprecated * Your contribution here. ### Removed * Your contribution here. ### Fixed * Your contribution here. ### Security * Your contribution here. ### Miscellaneous * Your contribution here. ``` Replace `` with the newly released versions to set up the compare view on Github. Increment the minor version, modify [lib/hashie/version.rb](lib/hashie/version.rb). Commit your changes. ```sh git add CHANGELOG.md README.md lib/hashie/version.rb git commit -m "Preparing for next development iteration, 3.3.1." git push origin master ``` hashie-5.0.0/Rakefile000066400000000000000000000014101414227716500144270ustar00rootroot00000000000000require 'rubygems' require 'bundler' Bundler.setup Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' RSpec::Core::RakeTask.new do |spec| spec.pattern = 'spec/**/*_spec.rb' spec.exclude_pattern = 'spec/integration/**/*_spec.rb' end require 'rubocop/rake_task' RuboCop::RakeTask.new(:rubocop) require_relative 'spec/support/integration_specs' task :integration_specs do next if ENV['CI'] status_codes = [] handler = lambda do |status_code| status_codes << status_code unless status_code.zero? end run_all_integration_specs(handler: handler, logger: ->(msg) { puts msg }) if status_codes.any? warn "#{status_codes.size} integration test(s) failed" exit status_codes.last end end task default: %i[rubocop spec integration_specs] hashie-5.0.0/UPGRADING.md000066400000000000000000000175561414227716500146460ustar00rootroot00000000000000Upgrading Hashie ================ ### Upgrading to 5.0.0 #### Mash initialization key conversion Mash initialization now only converts to string keys which can be represented as symbols. ```ruby Hashie::Mash.new( {foo: "bar"} => "baz", "1" => "one string", :"1" => "one sym", 1 => "one num" ) # Before {"{:foo=>\"bar\"}"=>"baz", "1"=>"one num"} # After {{:foo=>"bar"}=>"baz", "1"=>"one sym", 1=>"one num"} ``` #### Mash#dig with numeric keys `Hashie::Mash#dig` no longer considers numeric keys for indifferent access. ```ruby my_mash = Hashie::Mash.new("1" => "a") # => {"1"=>"a"} my_mash.dig("1") # => "a" my_mash.dig(:"1") # => "a" # Before my_mash.dig(1) # => "a" # After my_mash.dig(1) # => nil ``` ### Upgrading to 4.0.0 #### Non-destructive Hash methods called on Mash The following non-destructive Hash methods called on Mash will now return an instance of the class it was called on. | method | ruby | | ----------------- | ---- | | #compact | | | #invert | | | #reject | | | #select | | | #slice | 2.5 | | #transform_keys | 2.5 | | #transform_values | 2.4 | ```ruby class Parents < Hashie::Mash; end parents = Parents.new(father: 'Dad', mother: 'Mom') cool_parents = parents.transform_values { |v| v + v[-1] + 'io'} p cool_parents # before: {"father"=>"Daddio", "mother"=>"Mommio"} => {"father"=>"Daddio", "mother"=>"Mommio"} # after: # => {"father"=>"Dad", "mother"=>"Mom"} ``` This may make places where you had to re-make the Mash redundant, and may cause unintended side effects if your application was expecting a plain old ruby Hash. #### Ruby 2.6: Mash#merge and Mash#merge! In Ruby > 2.6.0, Hashie now supports passing multiple hash and Mash objects to Mash#merge and Mash#merge!. #### Hashie::Mash::CannotDisableMashWarnings error class is removed There shouldn't really be a case that anyone was relying on catching this specific error, but if so, they should change it to rescue Hashie::Extensions::KeyConflictWarning::CannotDisableMashWarnings ### Upgrading to 3.7.0 #### Mash#load takes options The `Hashie::Mash#load` method now accepts options, changing the interface of `Parser#initialize`. If you have a custom parser, you must update its `initialize` method. For example, `Hashie::Extensions::Parsers::YamlErbParser` now accepts `permitted_classes`, `permitted_symbols` and `aliases` options. Before: ```ruby class Hashie::Extensions::Parsers::YamlErbParser def initialize(file_path) @file_path = file_path end end ``` After: ```ruby class Hashie::Extensions::Parsers::YamlErbParser def initialize(file_path, options = {}) @file_path = file_path @options = options end end ``` Options can now be passed into `Mash#load`. ```ruby Mash.load(filename, permitted_classes: []) ``` ### Upgrading to 3.5.2 #### Disable logging in Mash subclasses If you subclass `Hashie::Mash`, you can now disable the logging we do about overriding existing methods with keys. This looks like: ```ruby class MyMash < Hashie::Mash disable_warnings end ``` ### Upgrading to 3.4.7 #### Procs as default values for Dash ```ruby class MyHash < Hashie::Dash property :time, default: -> { Time.now } end ``` In versions < 3.4.7 `Time.now` will be evaluated when `time` property is accessed directly first time. In version >= 3.4.7 `Time.now` is evaluated in time of object initialization. ### Upgrading to 3.4.4 #### Mash subclasses and reverse_merge ```ruby class MyMash < Hashie::Mash end ``` In versions >= 3.4.4 `MyMash#reverse_merge` returns an instance of `MyMash` but in previous versions it was a `Hashie::Mash` instance. ### Upgrading to 3.2.2 #### Testing if key defined In versions <= 3.2.1 Hash object being questioned doesn't return a boolean value as it's mentioned in README.md ```ruby class MyHash < Hash include Hashie::Extensions::MethodAccess end h = MyHash.new h.abc = 'def' h.abc # => 'def' h.abc? # => 'def' ``` In versions >= 3.2.2 it returns a boolean value ```ruby h.abc? # => true h.abb? # => false ``` ### Upgrading to 3.2.1 #### Possible coercion changes The improvements made to coercions in version 3.2.1 [issue #200](https://github.com/hashie/hashie/pull/200) do not break the documented API, but are significant enough that changes may effect undocumented side-effects. Applications that depended on those side-effects will need to be updated. **Change**: Type coercion no longer creates new objects if the input matches the target type. Previously coerced properties always resulted in the creation of a new object, even when it wasn't necessary. This had the effect of a `dup` or `clone` on coerced properties but not uncoerced ones. If necessary, `dup` or `clone` your own objects. Do not assume Hashie will do it for you. **Change**: Failed coercion attempts now raise Hashie::CoercionError. Hashie now raises a Hashie::CoercionError that details on the property that could not be coerced, the source and target type of the coercion, and the internal error. Previously only the internal error was raised. Applications that were attempting to rescuing the internal errors should be updated to rescue Hashie::CoercionError instead. ### Upgrading to 3.0 #### Compatibility with Rails 4 Strong Parameters Version 2.1 introduced support to prevent default Rails 4 mass-assignment protection behavior. This was [issue #89](https://github.com/hashie/hashie/issues/89), resolved in [#104](https://github.com/hashie/hashie/pull/104). In version 2.2 this behavior has been removed in [#147](https://github.com/hashie/hashie/pull/147) in favor of a mixin and finally extracted into a separate gem in Hashie 3.0. To enable 2.1 compatible behavior with Rails 4, use the [hashie_rails](http://rubygems.org/gems/hashie_rails) gem. ``` gem 'hashie_rails' ``` See [#154](https://github.com/hashie/hashie/pull/154) and [Mash and Rails 4 Strong Parameters](README.md#mash-and-rails-4-strong-parameters) for more details. #### Key Conversions in Hashie::Dash and Hashie::Trash Version 2.1 and older of Hashie::Dash and Hashie::Trash converted keys to strings by default. This is no longer the case in 2.2. Consider the following code. ```ruby class Person < Hashie::Dash property :name end p = Person.new(name: 'dB.') ``` Version 2.1 behaves as follows. ```ruby p.name # => 'dB.' p[:name] # => 'dB.' p['name'] # => 'dB.' # not what I put in p.inspect # => { 'name' => 'dB.' } p.to_hash # => { 'name' => 'dB.' } ``` It was not possible to achieve the behavior of preserving keys, as described in [issue #151](https://github.com/hashie/hashie/issues/151). Version 2.2 does not perform this conversion by default. ```ruby p.name # => 'dB.' p[:name] # => 'dB.' # p['name'] # => NoMethodError p.inspect # => { :name => 'dB.' } p.to_hash # => { :name => 'dB.' } ``` To enable behavior compatible with older versions, use `Hashie::Extensions::Dash::IndifferentAccess`. ```ruby class Person < Hashie::Dash include Hashie::Extensions::Dash::IndifferentAccess property :name end ``` #### Key Conversions in Hashie::Hash#to_hash Version 2.1 or older of Hash#to_hash converted keys to strings automatically. ```ruby instance = Hashie::Hash[first: 'First', 'last' => 'Last'] instance.to_hash # => { "first" => 'First', "last" => 'Last' } ``` It was possible to symbolize keys by passing `:symbolize_keys`, however it was not possible to retrieve the hash with initial key values. ```ruby instance.to_hash(symbolize_keys: true) # => { :first => 'First', :last => 'Last' } instance.to_hash(stringify_keys: true) # => { "first" => 'First', "last" => 'Last' } ``` Version 2.2 no longer converts keys by default. ```ruby instance = Hashie::Hash[first: 'First', 'last' => 'Last'] instance.to_hash # => { :first => 'First', "last" => 'Last' } ``` The behavior with `symbolize_keys` and `stringify_keys` is unchanged. See [#152](https://github.com/hashie/hashie/pull/152) for more information. hashie-5.0.0/benchmarks/000077500000000000000000000000001414227716500151035ustar00rootroot00000000000000hashie-5.0.0/benchmarks/keep_original_mash_keys.rb000066400000000000000000000011561414227716500223060ustar00rootroot00000000000000require_relative '../lib/hashie' require 'benchmark/ips' class KeepingMash < Hashie::Mash include Hashie::Extensions::Mash::KeepOriginalKeys end original = { test: 'value' } mash = Hashie::Mash.new(original) keeping_mash = KeepingMash.new(original) Benchmark.ips do |x| x.report('keep symbol') { keeping_mash.test } x.report('normal symbol') { mash.test } x.compare! end original = { 'test' => 'value' } mash = Hashie::Mash.new(original) keeping_mash = KeepingMash.new(original) Benchmark.ips do |x| x.report('keep string') { keeping_mash.test } x.report('normal string') { mash.test } x.compare! end hashie-5.0.0/benchmarks/mash_method_access.rb000077500000000000000000000003771414227716500212530ustar00rootroot00000000000000$LOAD_PATH.unshift('lib') require 'hashie' require 'benchmark/ips' mash = Hashie::Mash.new(test: 'value') Benchmark.ips do |x| x.hold!('tmp/mash_benchmark.json') x.report('before') { mash.test } x.report('after') { mash.test } x.compare! end hashie-5.0.0/benchmarks/permissive_respond_to.rb000077500000000000000000000017031414227716500220560ustar00rootroot00000000000000#!/usr/bin/env ruby $LOAD_PATH.unshift File.expand_path(File.join('..', 'lib'), __dir__) require 'hashie' require 'benchmark/ips' require 'benchmark/memory' permissive = Class.new(Hashie::Mash) Benchmark.memory do |x| x.report('Default') {} x.report('Make permissive') do permissive.include Hashie::Extensions::Mash::PermissiveRespondTo end end class PermissiveMash < Hashie::Mash include Hashie::Extensions::Mash::PermissiveRespondTo end Benchmark.ips do |x| x.report('Mash.new') { Hashie::Mash.new(a: 1) } x.report('Permissive.new') { PermissiveMash.new(a: 1) } x.compare! end Benchmark.ips do |x| x.report('Mash#attr=') { Hashie::Mash.new.a = 1 } x.report('Permissive#attr=') { PermissiveMash.new.a = 1 } x.compare! end mash = Hashie::Mash.new(a: 1) permissive = PermissiveMash.new(a: 1) Benchmark.ips do |x| x.report('Mash#attr= x2') { mash.a = 1 } x.report('Permissive#attr= x2') { permissive.a = 1 } x.compare! end hashie-5.0.0/bin/000077500000000000000000000000001414227716500135365ustar00rootroot00000000000000hashie-5.0.0/bin/console000077500000000000000000000005131414227716500151250ustar00rootroot00000000000000#!/usr/bin/env ruby require 'bundler/setup' require 'hashie' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require 'irb' IRB.start hashie-5.0.0/bin/setup000077500000000000000000000002121414227716500146170ustar00rootroot00000000000000#!/bin/bash set -euo pipefail IFS=$'\n\t' bundle install for dir in spec/integration/*; do pushd "$dir" bundle install popd done hashie-5.0.0/hashie.gemspec000066400000000000000000000021721414227716500155760ustar00rootroot00000000000000require File.expand_path('../lib/hashie/version', __FILE__) Gem::Specification.new do |gem| gem.name = 'hashie' gem.version = Hashie::VERSION gem.authors = ['Michael Bleigh', 'Jerry Cheung'] gem.email = ['michael@intridea.com', 'jollyjerry@gmail.com'] gem.description = 'Hashie is a collection of classes and mixins that make hashes more powerful.' gem.summary = 'Your friendly neighborhood hash library.' gem.homepage = 'https://github.com/hashie/hashie' gem.license = 'MIT' gem.require_paths = ['lib'] gem.files = %w[.yardopts CHANGELOG.md CONTRIBUTING.md LICENSE README.md UPGRADING.md] gem.files += %w[Rakefile hashie.gemspec] gem.files += Dir['lib/**/*.rb'] if gem.respond_to?(:metadata) gem.metadata = { 'bug_tracker_uri' => 'https://github.com/hashie/hashie/issues', 'changelog_uri' => 'https://github.com/hashie/hashie/blob/master/CHANGELOG.md', 'documentation_uri' => 'https://www.rubydoc.info/gems/hashie', 'source_code_uri' => 'https://github.com/hashie/hashie' } end gem.add_development_dependency 'bundler' end hashie-5.0.0/lib/000077500000000000000000000000001414227716500135345ustar00rootroot00000000000000hashie-5.0.0/lib/hashie.rb000066400000000000000000000056241414227716500153310ustar00rootroot00000000000000require 'hashie/logger' require 'hashie/version' module Hashie autoload :Clash, 'hashie/clash' autoload :Dash, 'hashie/dash' autoload :Hash, 'hashie/hash' autoload :Mash, 'hashie/mash' autoload :Trash, 'hashie/trash' autoload :Rash, 'hashie/rash' autoload :Array, 'hashie/array' autoload :Utils, 'hashie/utils' module Extensions autoload :Coercion, 'hashie/extensions/coercion' autoload :DeepMerge, 'hashie/extensions/deep_merge' autoload :IgnoreUndeclared, 'hashie/extensions/ignore_undeclared' autoload :IndifferentAccess, 'hashie/extensions/indifferent_access' autoload :MergeInitializer, 'hashie/extensions/merge_initializer' autoload :MethodAccess, 'hashie/extensions/method_access' autoload :MethodQuery, 'hashie/extensions/method_access' autoload :MethodReader, 'hashie/extensions/method_access' autoload :MethodWriter, 'hashie/extensions/method_access' autoload :StringifyKeys, 'hashie/extensions/stringify_keys' autoload :SymbolizeKeys, 'hashie/extensions/symbolize_keys' autoload :DeepFetch, 'hashie/extensions/deep_fetch' autoload :DeepFind, 'hashie/extensions/deep_find' autoload :DeepLocate, 'hashie/extensions/deep_locate' autoload :PrettyInspect, 'hashie/extensions/pretty_inspect' autoload :KeyConversion, 'hashie/extensions/key_conversion' autoload :MethodAccessWithOverride, 'hashie/extensions/method_access' autoload :StrictKeyAccess, 'hashie/extensions/strict_key_access' autoload :RubyVersion, 'hashie/extensions/ruby_version' autoload :RubyVersionCheck, 'hashie/extensions/ruby_version_check' module Parsers autoload :YamlErbParser, 'hashie/extensions/parsers/yaml_erb_parser' end module Dash autoload :IndifferentAccess, 'hashie/extensions/dash/indifferent_access' autoload :PropertyTranslation, 'hashie/extensions/dash/property_translation' autoload :Coercion, 'hashie/extensions/dash/coercion' autoload :PredefinedValues, 'hashie/extensions/dash/predefined_values' end module Mash autoload :KeepOriginalKeys, 'hashie/extensions/mash/keep_original_keys' autoload :PermissiveRespondTo, 'hashie/extensions/mash/permissive_respond_to' autoload :SafeAssignment, 'hashie/extensions/mash/safe_assignment' autoload :SymbolizeKeys, 'hashie/extensions/mash/symbolize_keys' autoload :DefineAccessors, 'hashie/extensions/mash/define_accessors' end module Array autoload :PrettyInspect, 'hashie/extensions/array/pretty_inspect' end end class << self include Hashie::Extensions::StringifyKeys::ClassMethods include Hashie::Extensions::SymbolizeKeys::ClassMethods end require 'hashie/railtie' if defined?(::Rails) end hashie-5.0.0/lib/hashie/000077500000000000000000000000001414227716500147755ustar00rootroot00000000000000hashie-5.0.0/lib/hashie/array.rb000066400000000000000000000010101414227716500164300ustar00rootroot00000000000000require 'hashie/extensions/array/pretty_inspect' require 'hashie/extensions/ruby_version_check' module Hashie class Array < ::Array include Hashie::Extensions::Array::PrettyInspect include Hashie::Extensions::RubyVersionCheck with_minimum_ruby('2.3.0') do def dig(*indexes) converted_indexes = indexes.map do |idx| begin Integer(idx) rescue ArgumentError idx end end super(*converted_indexes) end end end end hashie-5.0.0/lib/hashie/clash.rb000066400000000000000000000054761414227716500164300ustar00rootroot00000000000000require 'hashie/hash' module Hashie # # A Clash is a "Chainable Lazy Hash". Inspired by libraries such as Arel, # a Clash allows you to chain together method arguments to build a # hash, something that's especially useful if you're doing something # like constructing a complex options hash. Here's a basic example: # # c = Hashie::Clash.new.conditions(:foo => 'bar').order(:created_at) # c # => {:conditions => {:foo => 'bar'}, :order => :created_at} # # Clash provides another way to create sub-hashes by using bang notation. # You can dive into a sub-hash by providing a key with a bang and dive # back out again with the _end! method. Example: # # c = Hashie::Clash.new.conditions!.foo('bar').baz(123)._end!.order(:created_at) # c # => { conditions: { foo: 'bar', baz: 123 }, order: :created_at} # # Because the primary functionality of Clash is to build options objects, # all keys are converted to symbols since many libraries expect symbols explicitly # for keys. # class Clash < ::Hash class ChainError < ::StandardError; end # The parent Clash if this Clash was created via chaining. attr_reader :_parent # Initialize a new clash by passing in a Hash to # convert and, optionally, the parent to which this # Clash is chained. def initialize(other_hash = {}, parent = nil) @_parent = parent other_hash.each_pair do |k, v| self[k.to_sym] = v end end # Jump back up a level if you are using bang method # chaining. For example: # # c = Hashie::Clash.new.foo('bar') # c.baz!.foo(123) # => c[:baz] # c.baz!._end! # => c def _end! _parent end def id(*args) #:nodoc: method_missing(:id, *args) end def merge_store(key, *args) #:nodoc: case args.length when 1 val = args.first val = self.class.new(self[key]).merge(val) if self[key].is_a?(::Hash) && val.is_a?(::Hash) else val = args end self[key.to_sym] = val self end def method_missing(name, *args) #:nodoc: if args.empty? && name.to_s.end_with?('!') key = name[0...-1].to_sym case self[key] when NilClass self[key] = self.class.new({}, self) when Clash self[key] when Hash self[key] = self.class.new(self[key], self) else raise ChainError, 'Tried to chain into a non-hash key.' end elsif args.any? merge_store(name, *args) else super end end def respond_to_missing?(method_name, _include_private = false) method_name = method_name.to_s if method_name.end_with?('!') key = method_name[0...-1].to_sym [NilClass, Clash, Hash].include?(self[key].class) else true end end end end hashie-5.0.0/lib/hashie/dash.rb000066400000000000000000000164631414227716500162530ustar00rootroot00000000000000require 'hashie/hash' require 'set' module Hashie # A Dash is a 'defined' or 'discrete' Hash, that is, a Hash # that has a set of defined keys that are accessible (with # optional defaults) and only those keys may be set or read. # # Dashes are useful when you need to create a very simple # lightweight data object that needs even fewer options and # resources than something like a DataMapper resource. # # It is preferrable to a Struct because of the in-class # API for defining properties as well as per-property defaults. class Dash < Hash include Hashie::Extensions::PrettyInspect alias to_s inspect # Defines a property on the Dash. Options are # as follows: # # * :default - Specify a default value for this property, # to be returned before a value is set on the property in a new # Dash. # # * :required - Specify the value as required for this # property, to raise an error if a value is unset in a new or # existing Dash. If a Proc is provided, it will be run in the # context of the Dash instance. If a Symbol is provided, the # property it represents must not be nil. The property is only # required if the value is truthy. # # * :message - Specify custom error message for required property # def self.property(property_name, options = {}) properties << property_name if options.key?(:default) defaults[property_name] = options[:default] elsif defaults.key?(property_name) defaults.delete property_name end define_getter_for(property_name) define_setter_for(property_name) @subclasses.each { |klass| klass.property(property_name, options) } if defined? @subclasses condition = options.delete(:required) if condition message = options.delete(:message) || "is required for #{name}." required_properties[property_name] = { condition: condition, message: message } elsif options.key?(:message) raise ArgumentError, 'The :message option should be used with :required option.' end end class << self attr_reader :properties, :defaults attr_reader :getters attr_reader :required_properties end instance_variable_set('@properties', Set.new) instance_variable_set('@getters', Set.new) instance_variable_set('@defaults', {}) instance_variable_set('@required_properties', {}) def self.inherited(klass) super (@subclasses ||= Set.new) << klass klass.instance_variable_set('@properties', properties.dup) klass.instance_variable_set('@getters', getters.dup) klass.instance_variable_set('@defaults', defaults.dup) klass.instance_variable_set('@required_properties', required_properties.dup) end # Check to see if the specified property has already been # defined. def self.property?(name) properties.include? name end # Check to see if the specified property is # required. def self.required?(name) required_properties.key? name end private_class_method def self.define_getter_for(property_name) return if getters.include?(property_name) define_method(property_name) { |&block| self.[](property_name, &block) } getters << property_name end private_class_method def self.define_setter_for(property_name) setter = :"#{property_name}=" return if instance_methods.include?(setter) define_method(setter) { |value| self.[]=(property_name, value) } end # You may initialize a Dash with an attributes hash # just like you would many other kinds of data objects. def initialize(attributes = {}, &block) super(&block) initialize_attributes(attributes) assert_required_attributes_set! end alias _regular_reader [] alias _regular_writer []= private :_regular_reader, :_regular_writer # Retrieve a value from the Dash (will return the # property's default value if it hasn't been set). def [](property) assert_property_exists! property value = super(property) # If the value is a lambda, proc, or whatever answers to call, eval the thing! if value.is_a? Proc self[property] = value.call # Set the result of the call as a value else yield value if block_given? value end end # Set a value on the Dash in a Hash-like way. Only works # on pre-existing properties. def []=(property, value) assert_property_required! property, value assert_property_exists! property super(property, value) end def merge(other_hash) new_dash = dup other_hash.each do |k, v| new_dash[k] = block_given? ? yield(k, self[k], v) : v end new_dash end def merge!(other_hash) other_hash.each do |k, v| self[k] = block_given? ? yield(k, self[k], v) : v end self end def replace(other_hash) other_hash = self.class.defaults.merge(other_hash) (keys - other_hash.keys).each { |key| delete(key) } other_hash.each { |key, value| self[key] = value } self end def to_h defaults = ::Hash[self.class.properties.map { |prop| [prop, self.class.defaults[prop]] }] defaults.merge(self) end alias to_hash to_h def update_attributes!(attributes) update_attributes(attributes) self.class.defaults.each_pair do |prop, value| next unless fetch(prop, nil).nil? self[prop] = begin val = value.dup if val.is_a?(Proc) val.arity == 1 ? val.call(self) : val.call else val end rescue TypeError value end end assert_required_attributes_set! end private def initialize_attributes(attributes) return unless attributes cleaned_attributes = attributes.reject { |_attr, value| value.nil? } update_attributes!(cleaned_attributes) end def update_attributes(attributes) return unless attributes attributes.each_pair do |att, value| self[att] = value end end def assert_property_exists!(property) fail_no_property_error!(property) unless self.class.property?(property) end def assert_required_attributes_set! self.class.required_properties.each_key do |required_property| assert_property_set!(required_property) end end def assert_property_set!(property) fail_property_required_error!(property) if send(property).nil? && required?(property) end def assert_property_required!(property, value) fail_property_required_error!(property) if value.nil? && required?(property) end def fail_property_required_error!(property) raise ArgumentError, "The property '#{property}' #{self.class.required_properties[property][:message]}" end def fail_no_property_error!(property) raise NoMethodError, "The property '#{property}' is not defined for #{self.class.name}." end def required?(property) return false unless self.class.required?(property) condition = self.class.required_properties[property][:condition] case condition when Proc then !!instance_exec(&condition) when Symbol then !!send(condition) else !!condition end end end end hashie-5.0.0/lib/hashie/extensions/000077500000000000000000000000001414227716500171745ustar00rootroot00000000000000hashie-5.0.0/lib/hashie/extensions/active_support/000077500000000000000000000000001414227716500222435ustar00rootroot00000000000000hashie-5.0.0/lib/hashie/extensions/active_support/core_ext/000077500000000000000000000000001414227716500240535ustar00rootroot00000000000000hashie-5.0.0/lib/hashie/extensions/active_support/core_ext/hash.rb000066400000000000000000000004241414227716500253230ustar00rootroot00000000000000module Hashie module Extensions module ActiveSupport module CoreExt module Hash def except(*keys) string_keys = keys.map { |key| convert_key(key) } super(*string_keys) end end end end end end hashie-5.0.0/lib/hashie/extensions/array/000077500000000000000000000000001414227716500203125ustar00rootroot00000000000000hashie-5.0.0/lib/hashie/extensions/array/pretty_inspect.rb000066400000000000000000000006571414227716500237230ustar00rootroot00000000000000module Hashie module Extensions module Array module PrettyInspect def self.included(base) base.send :alias_method, :array_inspect, :inspect base.send :alias_method, :inspect, :hashie_inspect end def hashie_inspect ret = "#<#{self.class} [" ret << to_a.map(&:inspect).join(', ') ret << ']>' ret end end end end end hashie-5.0.0/lib/hashie/extensions/coercion.rb000066400000000000000000000157271414227716500213360ustar00rootroot00000000000000module Hashie class CoercionError < StandardError def initialize(key, value, into, message) super("Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{message}") end end module Extensions module Coercion CORE_TYPES = { Integer => :to_i, Float => :to_f, Complex => :to_c, Rational => :to_r, String => :to_s, Symbol => :to_sym }.freeze ABSTRACT_CORE_TYPES = if RubyVersion.new(RUBY_VERSION) >= RubyVersion.new('2.4.0') { Numeric => [Integer, Float, Complex, Rational] } else { Integer => [Fixnum, Bignum], Numeric => [Fixnum, Bignum, Float, Complex, Rational] } end def self.included(base) base.send :include, InstanceMethods base.extend ClassMethods unless base.method_defined?(:set_value_without_coercion) base.send :alias_method, :set_value_without_coercion, :[]= end base.send :alias_method, :[]=, :set_value_with_coercion end module InstanceMethods def set_value_with_coercion(key, value) into = self.class.key_coercion(key) || self.class.value_coercion(value) unless value.nil? || into.nil? begin value = self.class.fetch_coercion(into).call(value) rescue NoMethodError, TypeError => e raise CoercionError.new(key, value, into, e.message) end end set_value_without_coercion(key, value) end def custom_writer(key, value, _convert = true) self[key] = value end def replace(other_hash) (keys - other_hash.keys).each { |key| delete(key) } other_hash.each { |key, value| self[key] = value } self end end module ClassMethods attr_writer :key_coercions protected :key_coercions= # Set up a coercion rule such that any time the specified # key is set it will be coerced into the specified class. # Coercion will occur by first attempting to call Class.coerce # and then by calling Class.new with the value as an argument # in either case. # # @param [Object] key the key or array of keys you would like to be coerced. # @param [Class] into the class into which you want the key(s) coerced. # # @example Coerce a "user" subhash into a User object # class Tweet < Hash # include Hashie::Extensions::Coercion # coerce_key :user, User # end def coerce_key(*attrs) into = attrs.pop attrs.each { |key| key_coercions[key] = into } end alias coerce_keys coerce_key # Returns a hash of any existing key coercions. def key_coercions @key_coercions ||= {} end # Returns the specific key coercion for the specified key, # if one exists. def key_coercion(key) key_coercions[key.to_sym] end # Set up a coercion rule such that any time a value of the # specified type is set it will be coerced into the specified # class. # # @param [Class] from the type you would like coerced. # @param [Class] into the class into which you would like the value coerced. # @option options [Boolean] :strict (true) whether use exact source class # only or include ancestors # # @example Coerce all hashes into this special type of hash # class SpecialHash < Hash # include Hashie::Extensions::Coercion # coerce_value Hash, SpecialHash # # def initialize(hash = {}) # super # hash.each_pair do |k,v| # self[k] = v # end # end # end def coerce_value(from, into, options = {}) options = { strict: true }.merge(options) if ABSTRACT_CORE_TYPES.key? from ABSTRACT_CORE_TYPES[from].each do |type| coerce_value type, into, options end end if options[:strict] strict_value_coercions[from] = into else while from.superclass && from.superclass != Object lenient_value_coercions[from] = into from = from.superclass end end end # Return all value coercions that have the :strict rule as true. def strict_value_coercions @strict_value_coercions ||= {} end # Return all value coercions that have the :strict rule as false. def lenient_value_coercions @lenient_value_coercions ||= {} end # Fetch the value coercion, if any, for the specified object. def value_coercion(value) from = value.class strict_value_coercions[from] || lenient_value_coercions[from] end def fetch_coercion(type) return type if type.is_a? Proc coercion_cache[type] end def coercion_cache @coercion_cache ||= ::Hash.new do |hash, type| hash[type] = build_coercion(type) end end def build_coercion(type) if type.is_a? Enumerable if type.class == ::Hash type, key_type, value_type = type.class, *type.first build_hash_coercion(type, key_type, value_type) else value_type = type.first type = type.class build_container_coercion(type, value_type) end elsif CORE_TYPES.key? type build_core_type_coercion(type) elsif type.respond_to? :coerce lambda do |value| return value if value.is_a? type type.coerce(value) end elsif type.respond_to? :new lambda do |value| return value if value.is_a? type type.new(value) end else raise TypeError, "#{type} is not a coercable type" end end def build_hash_coercion(type, key_type, value_type) key_coerce = fetch_coercion(key_type) value_coerce = fetch_coercion(value_type) lambda do |value| type[value.map { |k, v| [key_coerce.call(k), value_coerce.call(v)] }] end end def build_container_coercion(type, value_type) value_coerce = fetch_coercion(value_type) lambda do |value| type.new(value.map { |v| value_coerce.call(v) }) end end def build_core_type_coercion(type) name = CORE_TYPES[type] lambda do |value| return value if value.is_a? type return value.send(name) end end def inherited(klass) super klass.key_coercions = key_coercions.dup end end end end end hashie-5.0.0/lib/hashie/extensions/dash/000077500000000000000000000000001414227716500201135ustar00rootroot00000000000000hashie-5.0.0/lib/hashie/extensions/dash/coercion.rb000066400000000000000000000013431414227716500222420ustar00rootroot00000000000000module Hashie module Extensions module Dash module Coercion # Extends a Dash with the ability to define coercion for properties. def self.included(base) base.send :include, Hashie::Extensions::Coercion base.extend ClassMethods end module ClassMethods # Defines a property on the Dash. Options are the standard # Hashie::Dash#property options plus: # # * :coerce - The class into which you want the property coerced. def property(property_name, options = {}) super coerce_key property_name, options[:coerce] if options[:coerce] end end end end end end hashie-5.0.0/lib/hashie/extensions/dash/indifferent_access.rb000066400000000000000000000035731414227716500242660ustar00rootroot00000000000000module Hashie module Extensions module Dash module IndifferentAccess def self.included(base) base.extend ClassMethods base.send :include, Hashie::Extensions::IndifferentAccess end def self.maybe_extend(base) return unless requires_class_methods?(base) base.extend(ClassMethods) end def self.requires_class_methods?(klass) klass <= Hashie::Dash && !klass.singleton_class.included_modules.include?(ClassMethods) end private_class_method :requires_class_methods? def to_h defaults = ::Hash[self.class.properties.map do |prop| [Hashie::Extensions::IndifferentAccess.convert_key(prop), self.class.defaults[prop]] end] defaults.merge(self) end alias to_hash to_h module ClassMethods # Check to see if the specified property has already been # defined. def property?(name) name = translations[name.to_sym] if translation_for?(name) name = name.to_s !!properties.find { |property| property.to_s == name } end def translation_exists?(name) name = name.to_s !!translations.keys.find { |key| key.to_s == name } end def transformed_property(property_name, value) transform = transforms[property_name] || transforms[property_name.to_sym] transform.call(value) end def transformation_exists?(name) name = name.to_s !!transforms.keys.find { |key| key.to_s == name } end private def translation_for?(name) included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) && translation_exists?(name) end end end end end end hashie-5.0.0/lib/hashie/extensions/dash/predefined_values.rb000066400000000000000000000050501414227716500241240ustar00rootroot00000000000000module Hashie module Extensions module Dash # Extends a Dash with the ability to accept only predefined values on a property. # # == Example # # class PersonHash < Hashie::Dash # include Hashie::Extensions::Dash::PredefinedValues # # property :gender, values: [:male, :female, :prefer_not_to_say] # property :age, values: (0..150) # a Range # end # # person = PersonHash.new(gender: :male, age: -1) # # => ArgumentError: The value '-1' is not accepted for property 'age' module PredefinedValues def self.included(base) base.instance_variable_set(:@values_for_properties, {}) base.extend(ClassMethods) base.include(InstanceMethods) end module ClassMethods attr_reader :values_for_properties def inherited(klass) super klass.instance_variable_set(:@values_for_properties, values_for_properties.dup) end def property(property_name, options = {}) super return unless (predefined_values = options[:values]) assert_predefined_values!(predefined_values) set_predefined_values(property_name, predefined_values) end private def assert_predefined_values!(predefined_values) return if supported_type?(predefined_values) raise ArgumentError, %(`values` accepts an Array or a Range.) end def supported_type?(predefined_values) [::Array, ::Range].any? { |klass| predefined_values.is_a?(klass) } end def set_predefined_values(property_name, predefined_values) @values_for_properties[property_name] = predefined_values end end module InstanceMethods def initialize(*) super assert_property_values! end private def assert_property_values! self.class.values_for_properties.each_key do |property| value = send(property) if value && !values_for_properties(property).include?(value) fail_property_value_error!(property) end end end def fail_property_value_error!(property) raise ArgumentError, "Invalid value for property '#{property}'" end def values_for_properties(property) self.class.values_for_properties[property] end end end end end end hashie-5.0.0/lib/hashie/extensions/dash/property_translation.rb000066400000000000000000000157551414227716500247570ustar00rootroot00000000000000module Hashie module Extensions module Dash # Extends a Dash with the ability to remap keys from a source hash. # # Property translation is useful when you need to read data from another # application -- such as a Java API -- where the keys are named # differently from Ruby conventions. # # == Example from inconsistent APIs # # class PersonHash < Hashie::Dash # include Hashie::Extensions::Dash::PropertyTranslation # # property :first_name, from :firstName # property :last_name, from: :lastName # property :first_name, from: :f_name # property :last_name, from: :l_name # end # # person = PersonHash.new(firstName: 'Michael', l_name: 'Bleigh') # person[:first_name] #=> 'Michael' # person[:last_name] #=> 'Bleigh' # # You can also use a lambda to translate the value. This is particularly # useful when you want to ensure the type of data you're wrapping. # # == Example using translation lambdas # # class DataModelHash < Hashie::Dash # include Hashie::Extensions::Dash::PropertyTranslation # # property :id, transform_with: ->(value) { value.to_i } # property :created_at, from: :created, with: ->(value) { Time.parse(value) } # end # # model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28') # model.id.class #=> Integer (Fixnum if you are using Ruby 2.3 or lower) # model.created_at.class #=> Time module PropertyTranslation def self.included(base) base.instance_variable_set(:@transforms, {}) base.instance_variable_set(:@translations_hash, ::Hash.new { |hash, key| hash[key] = {} }) base.extend(ClassMethods) base.send(:include, InstanceMethods) end module ClassMethods attr_reader :transforms, :translations_hash # Ensures that any inheriting classes maintain their translations. # # * :default - The class inheriting the translations. def inherited(klass) super klass.instance_variable_set(:@transforms, transforms.dup) klass.instance_variable_set(:@translations_hash, translations_hash.dup) end def permitted_input_keys @permitted_input_keys ||= properties .map { |property| inverse_translations.fetch property, property } end # Defines a property on the Trash. Options are as follows: # # * :default - Specify a default value for this property, to be # returned before a value is set on the property in a new Dash. # * :from - Specify the original key name that will be write only. # * :with - Specify a lambda to be used to convert value. # * :transform_with - Specify a lambda to be used to convert value # without using the :from option. It transform the property itself. def property(property_name, options = {}) super from = options[:from] converter = options[:with] transformer = options[:transform_with] if from fail_self_transformation_error!(property_name) if property_name == from define_translation(from, property_name, converter || transformer) define_writer_for_source_property(from) elsif valid_transformer?(transformer) transforms[property_name] = transformer end end def transformed_property(property_name, value) transforms[property_name].call(value) end def transformation_exists?(name) transforms.key? name end def translation_exists?(name) translations_hash.key? name end def translations @translations ||= {}.tap do |translations| translations_hash.each do |(property_name, property_translations)| translations[property_name] = if property_translations.size > 1 property_translations.keys else property_translations.keys.first end end end end def inverse_translations @inverse_translations ||= {}.tap do |translations| translations_hash.each do |(property_name, property_translations)| property_translations.each_key do |key| translations[key] = property_name end end end end private def define_translation(from, property_name, translator) translations_hash[from][property_name] = translator end def define_writer_for_source_property(property) define_method "#{property}=" do |val| __translations[property].each do |name, with| self[name] = with.respond_to?(:call) ? with.call(val) : val end end end def fail_self_transformation_error!(property_name) raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same" end def valid_transformer?(transformer) transformer.respond_to? :call end end module InstanceMethods # Sets a value on the Dash in a Hash-like way. # # Note: Only works on pre-existing properties. def []=(property, value) if self.class.translation_exists? property send("#{property}=", value) if self.class.transformation_exists? property super property, self.class.transformed_property(property, value) elsif self.class.properties.include?(property) super(property, value) end elsif self.class.transformation_exists? property super property, self.class.transformed_property(property, value) elsif property_exists? property super end end # Deletes any keys that have a translation def initialize_attributes(attributes) return unless attributes attributes_copy = attributes.dup.delete_if do |k, v| if self.class.translations_hash.include?(k) self[k] = v true end end super attributes_copy end # Raises an NoMethodError if the property doesn't exist def property_exists?(property) fail_no_property_error!(property) unless self.class.property?(property) true end private def __translations self.class.translations_hash end end end end end end hashie-5.0.0/lib/hashie/extensions/deep_fetch.rb000066400000000000000000000021001414227716500216000ustar00rootroot00000000000000module Hashie module Extensions # Searches a deeply nested datastructure for a key path, and returns the associated value. # # options = { user: { location: { address: '123 Street' } } } # options.deep_fetch :user, :location, :address #=> '123 Street' # # If a block is provided its value will be returned if the key does not exist. # # options.deep_fetch(:user, :non_existent_key) { 'a value' } #=> 'a value' # # This is particularly useful for fetching values from deeply nested api responses # or params hashes. module DeepFetch class UndefinedPathError < StandardError; end def deep_fetch(*args, &block) args.reduce(self) do |obj, arg| begin arg = Integer(arg) if obj.is_a? Array obj.fetch(arg) rescue ArgumentError, IndexError, NoMethodError => e break yield(arg) if block raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace end end end end end end hashie-5.0.0/lib/hashie/extensions/deep_find.rb000066400000000000000000000040311414227716500214340ustar00rootroot00000000000000require 'hashie/extensions/deep_locate' module Hashie module Extensions module DeepFind # Performs a depth-first search on deeply nested data structures for # a key and returns the first occurrence of the key. # # options = {user: {location: {address: '123 Street'}}} # options.extend(Hashie::Extensions::DeepFind) # options.deep_find(:address) # => '123 Street' # # class MyHash < Hash # include Hashie::Extensions::DeepFind # end # # my_hash = MyHash.new # my_hash[:user] = {location: {address: '123 Street'}} # my_hash.deep_find(:address) # => '123 Street' def deep_find(key) _deep_find(key) end alias deep_detect deep_find # Performs a depth-first search on deeply nested data structures for # a key and returns all occurrences of the key. # # options = { # users: [ # { location: {address: '123 Street'} }, # { location: {address: '234 Street'}} # ] # } # options.extend(Hashie::Extensions::DeepFind) # options.deep_find_all(:address) # => ['123 Street', '234 Street'] # # class MyHash < Hash # include Hashie::Extensions::DeepFind # end # # my_hash = MyHash.new # my_hash[:users] = [ # {location: {address: '123 Street'}}, # {location: {address: '234 Street'}} # ] # my_hash.deep_find_all(:address) # => ['123 Street', '234 Street'] def deep_find_all(key) matches = _deep_find_all(key) matches.empty? ? nil : matches end alias deep_select deep_find_all private def _deep_find(key, object = self) _deep_find_all(key, object).first end def _deep_find_all(key, object = self, matches = []) deep_locate_result = DeepLocate.deep_locate(key, object).tap do |result| result.map! { |element| element[key] } end matches.concat(deep_locate_result) end end end end hashie-5.0.0/lib/hashie/extensions/deep_locate.rb000066400000000000000000000072641414227716500217760ustar00rootroot00000000000000module Hashie module Extensions module DeepLocate # The module level implementation of #deep_locate, incase you do not want # to include/extend the base datastructure. For further examples please # see #deep_locate. # # @example # books = [ # { # title: "Ruby for beginners", # pages: 120 # }, # ... # ] # # DeepLocate.deep_locate -> (key, value, object) { key == :title }, books # # => [{:title=>"Ruby for beginners", :pages=>120}, ...] def self.deep_locate(comparator, object) unless comparator.respond_to?(:call) comparator = _construct_key_comparator(comparator, object) end _deep_locate(comparator, object) end # Performs a depth-first search on deeply nested data structures for a # given comparator callable and returns each Enumerable, for which the # callable returns true for at least one the its elements. # # @example # books = [ # { # title: "Ruby for beginners", # pages: 120 # }, # { # title: "CSS for intermediates", # pages: 80 # }, # { # title: "Collection of ruby books", # books: [ # { # title: "Ruby for the rest of us", # pages: 576 # } # ] # } # ] # # books.extend(Hashie::Extensions::DeepLocate) # # # for ruby 1.9 leave *no* space between the lambda rocket and the braces # # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/ # # books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") } # # => [{:title=>"Ruby for beginners", :pages=>120}, # # {:title=>"Ruby for the rest of us", :pages=>576}] # # books.deep_locate -> (key, value, object) { key == :pages && value <= 120 } # # => [{:title=>"Ruby for beginners", :pages=>120}, # # {:title=>"CSS for intermediates", :pages=>80}] def deep_locate(comparator) Hashie::Extensions::DeepLocate.deep_locate(comparator, self) end def self._construct_key_comparator(search_key, object) if object.respond_to?(:indifferent_access?) && object.indifferent_access? || activesupport_indifferent?(object) search_key = search_key.to_s end lambda do |non_callable_object| ->(key, _, _) { key == non_callable_object } end.call(search_key) end private_class_method :_construct_key_comparator def self._deep_locate(comparator, object, result = []) if object.is_a?(::Enumerable) if object.any? { |value| _match_comparator?(value, comparator, object) } result.push object end (object.respond_to?(:values) ? object.values : object.entries).each do |value| _deep_locate(comparator, value, result) end end result end private_class_method :_deep_locate def self._match_comparator?(value, comparator, object) if object.is_a?(::Hash) key, value = value else key = nil end comparator.call(key, value, object) end private_class_method :_match_comparator? def self.activesupport_indifferent?(object) defined?(::ActiveSupport::HashWithIndifferentAccess) && object.is_a?(::ActiveSupport::HashWithIndifferentAccess) end private_class_method :activesupport_indifferent? end end end hashie-5.0.0/lib/hashie/extensions/deep_merge.rb000066400000000000000000000026751414227716500216270ustar00rootroot00000000000000module Hashie module Extensions module DeepMerge # Returns a new hash with +self+ and +other_hash+ merged recursively. def deep_merge(other_hash, &block) copy = _deep_dup(self) copy.extend(Hashie::Extensions::DeepMerge) unless copy.respond_to?(:deep_merge!) copy.deep_merge!(other_hash, &block) end # Returns a new hash with +self+ and +other_hash+ merged recursively. # Modifies the receiver in place. def deep_merge!(other_hash, &block) return self unless other_hash.is_a?(::Hash) _recursive_merge(self, other_hash, &block) self end private def _deep_dup(hash) copy = hash.dup copy.each do |key, value| copy[key] = if value.is_a?(::Hash) _deep_dup(value) else Hashie::Utils.safe_dup(value) end end copy end def _recursive_merge(hash, other_hash, &block) other_hash.each do |k, v| hash[k] = if hash.key?(k) && hash[k].is_a?(::Hash) && v.is_a?(::Hash) _recursive_merge(hash[k], v, &block) elsif v.is_a?(::Hash) _recursive_merge({}, v, &block) elsif hash.key?(k) && block_given? yield(k, hash[k], v) else v.respond_to?(:deep_dup) ? v.deep_dup : v end end hash end end end end hashie-5.0.0/lib/hashie/extensions/ignore_undeclared.rb000066400000000000000000000025711414227716500231770ustar00rootroot00000000000000module Hashie module Extensions # IgnoreUndeclared is a simple mixin that silently ignores # undeclared properties on initialization instead of # raising an error. This is useful when using a Trash to # capture a subset of a larger hash. # # Note that attempting to retrieve or set an undeclared property # will still raise a NoMethodError, even if a value for # that property was provided at initialization. # # @example # class Person < Trash # include Hashie::Extensions::IgnoreUndeclared # # property :first_name # property :last_name # end # # user_data = { # :first_name => 'Freddy', # :last_name => 'Nostrils', # :email => 'freddy@example.com' # } # # p = Person.new(user_data) # 'email' is silently ignored # # p.first_name # => 'Freddy' # p.last_name # => 'Nostrils' # p.email # => NoMethodError module IgnoreUndeclared def initialize_attributes(attributes) return unless attributes klass = self.class translations = klass.respond_to?(:translations) && klass.translations || [] super(attributes.select { |attr, _| klass.property?(attr) || translations.include?(attr) }) end def property_exists?(property) self.class.property?(property) end end end end hashie-5.0.0/lib/hashie/extensions/indifferent_access.rb000066400000000000000000000123331414227716500233410ustar00rootroot00000000000000module Hashie module Extensions # IndifferentAccess gives you the ability to not care # whether your hash has string or symbol keys. Made famous # in Rails for accessing query and POST parameters, this # is a handy tool for making sure your hash has maximum # utility. # # One unique feature of this mixin is that it will recursively # inject itself into sub-hash instances without modifying # the actual class of the sub-hash. # # @example # class MyHash < Hash # include Hashie::Extensions::MergeInitializer # include Hashie::Extensions::IndifferentAccess # end # # h = MyHash.new(:foo => 'bar', 'baz' => 'blip') # h['foo'] # => 'bar' # h[:foo] # => 'bar' # h[:baz] # => 'blip' # h['baz'] # => 'blip' # module IndifferentAccess include Hashie::Extensions::RubyVersionCheck # @api private def self.convert_key(key) key.to_s end def self.included(base) Hashie::Extensions::Dash::IndifferentAccess.maybe_extend(base) base.class_eval do alias_method :regular_writer, :[]= unless method_defined?(:regular_writer) alias_method :[]=, :indifferent_writer alias_method :store, :indifferent_writer %w[default update replace fetch delete key? values_at].each do |m| alias_method "regular_#{m}", m unless method_defined?("regular_#{m}") alias_method m, "indifferent_#{m}" end %w[include? member? has_key?].each do |key_alias| alias_method key_alias, :indifferent_key? end class << self def [](*) super.convert! end def try_convert(*) (hash = super) && self[hash] end end end end # This will inject indifferent access into an instance of # a hash without modifying the actual class. This is what # allows IndifferentAccess to spread to sub-hashes. def self.inject!(hash) (class << hash; self; end).send :include, IndifferentAccess hash.convert! end # Injects indifferent access into a duplicate of the hash # provided. See #inject! def self.inject(hash) inject!(hash.dup) end def convert_key(key) IndifferentAccess.convert_key(key) end # Iterates through the keys and values, reconverting them to # their proper indifferent state. Used when IndifferentAccess # is injecting itself into member hashes. def convert! keys.each do |k| # rubocop:disable Performance/HashEachMethods indifferent_writer k, regular_delete(k) end self end def indifferent_value(value) if hash_lacking_indifference?(value) IndifferentAccess.inject!(value) elsif value.is_a?(::Array) value.replace(value.map { |e| indifferent_value(e) }) else value end end def indifferent_default(key = nil) return self[convert_key(key)] if key?(key) regular_default(key) end def indifferent_update(other_hash) return regular_update(other_hash) if hash_with_indifference?(other_hash) other_hash.each_pair do |k, v| self[k] = v end end def indifferent_writer(key, value) regular_writer convert_key(key), indifferent_value(value) end def indifferent_fetch(key, *args, &block) regular_fetch convert_key(key), *args, &block end def indifferent_delete(key) regular_delete convert_key(key) end def indifferent_key?(key) regular_key? convert_key(key) end def indifferent_values_at(*indices) indices.map { |i| self[i] } end def indifferent_access? true end def indifferent_replace(other_hash) (keys - other_hash.keys).each { |key| delete(key) } other_hash.each { |key, value| self[key] = value } self end def merge(*args) result = super return IndifferentAccess.inject!(result) if hash_lacking_indifference?(result) result.convert! end def merge!(*) super.convert! end def to_hash {}.tap do |result| each_pair { |key, value| result[key] = value } if default_proc result.default_proc = default_proc else result.default = default end end end with_minimum_ruby('2.5.0') do def slice(*keys) string_keys = keys.map { |key| convert_key(key) } super(*string_keys) end end with_minimum_ruby('3.0.0') do def except(*keys) string_keys = keys.map { |key| convert_key(key) } super(*string_keys) end end protected def hash_lacking_indifference?(other) other.is_a?(::Hash) && !(other.respond_to?(:indifferent_access?) && other.indifferent_access?) end def hash_with_indifference?(other) other.is_a?(::Hash) && other.respond_to?(:indifferent_access?) && other.indifferent_access? end end end end hashie-5.0.0/lib/hashie/extensions/key_conflict_warning.rb000066400000000000000000000030531414227716500237200ustar00rootroot00000000000000module Hashie module Extensions module KeyConflictWarning class CannotDisableMashWarnings < StandardError def initialize super( 'You cannot disable warnings on the base Mash class. ' \ 'Please subclass the Mash and disable it in the subclass.' ) end end # Disable the logging of warnings based on keys conflicting keys/methods # # @api semipublic # @return [void] def disable_warnings(*method_keys) raise CannotDisableMashWarnings if self == Hashie::Mash if method_keys.any? disabled_warnings.concat(method_keys).tap(&:flatten!).uniq! else disabled_warnings.clear end @disable_warnings = true end # Checks whether this class disables warnings for conflicting keys/methods # # @api semipublic # @return [Boolean] def disable_warnings?(method_key = nil) return disabled_warnings.include?(method_key) if disabled_warnings.any? && method_key @disable_warnings ||= false end # Returns an array of methods that this class disables warnings for. # # @api semipublic # @return [Boolean] def disabled_warnings @_disabled_warnings ||= [] end # Inheritance hook that sets class configuration when inherited. # # @api semipublic # @return [void] def inherited(subclass) super subclass.disable_warnings(disabled_warnings) if disable_warnings? end end end end hashie-5.0.0/lib/hashie/extensions/key_conversion.rb000066400000000000000000000003111414227716500225510ustar00rootroot00000000000000module Hashie module Extensions module KeyConversion def self.included(base) base.send :include, SymbolizeKeys base.send :include, StringifyKeys end end end end hashie-5.0.0/lib/hashie/extensions/mash/000077500000000000000000000000001414227716500201245ustar00rootroot00000000000000hashie-5.0.0/lib/hashie/extensions/mash/define_accessors.rb000066400000000000000000000047071414227716500237600ustar00rootroot00000000000000module Hashie module Extensions module Mash module DefineAccessors def self.included(klass) klass.class_eval do mod = Ext.new include mod end end def self.extended(obj) included(obj.singleton_class) end class Ext < Module def initialize mod = self define_method(:method_missing) do |method_name, *args, &block| key, suffix = method_name_and_suffix(method_name) case suffix when '='.freeze mod.define_writer(key, method_name) when '?'.freeze mod.define_predicate(key, method_name) when '!'.freeze mod.define_initializing_reader(key, method_name) when '_'.freeze mod.define_underbang_reader(key, method_name) else mod.define_reader(key, method_name) end send(method_name, *args, &block) end end def define_reader(key, method_name) define_method(method_name) do |&block| if key? method_name self.[](method_name, &block) else self.[](key, &block) end end end def define_writer(key, method_name) define_method(method_name) do |value = nil| if key? method_name self.[](method_name, &proc) else assign_property(key, value) end end end def define_predicate(key, method_name) define_method(method_name) do if key? method_name self.[](method_name, &proc) else !!self[key] end end end def define_initializing_reader(key, method_name) define_method(method_name) do if key? method_name self.[](method_name, &proc) else initializing_reader(key) end end end def define_underbang_reader(key, method_name) define_method(method_name) do if key? method_name self.[](key, &proc) else underbang_reader(key) end end end end end end end end hashie-5.0.0/lib/hashie/extensions/mash/keep_original_keys.rb000066400000000000000000000033071414227716500243170ustar00rootroot00000000000000module Hashie module Extensions module Mash # Overrides the indifferent access of a Mash to keep keys in the # original format given to the Mash. # # @example # class KeepingMash < Hashie::Mash # include Hashie::Extensions::Mash::KeepOriginalKeys # end # # mash = KeepingMash.new(:symbol_key => :symbol, 'string_key' => 'string') # mash.to_hash #=> { :symbol_key => :symbol, 'string_key' => 'string' } # mash['string_key'] == mash[:string_key] #=> true # mash[:symbol_key] == mash['symbol_key'] #=> true module KeepOriginalKeys def self.included(descendant) error_message = "#{descendant} is not a kind of Hashie::Mash" raise ArgumentError, error_message unless descendant <= Hashie::Mash end private # Converts the key when necessary to access the correct Mash key. # # @param [Object, String, Symbol] key the key to access. # @return [Object] the value assigned to the key. def convert_key(key) if regular_key?(key) key elsif (converted_key = __convert(key)) && regular_key?(converted_key) converted_key else key end end # Converts symbol/string keys to their alternative formats, but leaves # other keys alone. # # @param [Object, String, Symbol] key the key to convert. # @return [Object, String, Symbol] the converted key. def __convert(key) case key when Symbol then key.to_s when String then key.to_sym else key end end end end end end hashie-5.0.0/lib/hashie/extensions/mash/permissive_respond_to.rb000066400000000000000000000042611414227716500250760ustar00rootroot00000000000000module Hashie module Extensions module Mash # Allow a Mash to properly respond to everything # # By default, Mashes only say they respond to methods for keys that exist # in their key set or any of the affix methods (e.g. setter, underbang, # etc.). This causes issues when you try to use them within a # SimpleDelegator or bind to a method for a key that is unset. # # This extension allows a Mash to properly respond to `respond_to?` and # `method` for keys that have not yet been set. This enables full # compatibility with SimpleDelegator and thunk-oriented programming. # # There is a trade-off with this extension: it will run slower than a # regular Mash; insertions and initializations with keys run approximately # 20% slower and cost approximately 19KB of memory per class that you # make permissive. # # @api public # @example Make a new, permissively responding Mash subclass # class PermissiveMash < Hashie::Mash # include Hashie::Extensions::Mash::PermissiveRespondTo # end # # mash = PermissiveMash.new(a: 1) # mash.respond_to? :b #=> true module PermissiveRespondTo # The Ruby hook for behavior when including the module # # @api private # @private # @return void def self.included(base) base.instance_variable_set :@_method_cache, base.instance_methods base.define_singleton_method(:method_cache) { @_method_cache } end # The Ruby hook for determining what messages a class might respond to # # @api private # @private def respond_to_missing?(_method_name, _include_private = false) true end private # Override the Mash logging behavior to account for permissiveness # # @api private # @private def log_collision?(method_key) self.class.method_cache.include?(method_key) && !self.class.disable_warnings?(method_key) && !(regular_key?(method_key) || regular_key?(method_key.to_s)) end end end end end hashie-5.0.0/lib/hashie/extensions/mash/safe_assignment.rb000066400000000000000000000006201414227716500236150ustar00rootroot00000000000000module Hashie module Extensions module Mash module SafeAssignment def custom_writer(key, *args) #:nodoc: if !key?(key) && respond_to?(key, true) raise ArgumentError, "The property #{key} clashes with an existing method." end super end def []=(*args) custom_writer(*args) end end end end end hashie-5.0.0/lib/hashie/extensions/mash/symbolize_keys.rb000066400000000000000000000020571414227716500235250ustar00rootroot00000000000000module Hashie module Extensions module Mash # Overrides Mash's default behavior of converting keys to strings # # @example # class LazyResponse < Hashie::Mash # include Hashie::Extensions::Mash::SymbolizeKeys # end # # response = LazyResponse.new("id" => 123, "name" => "Rey").to_h # #=> {id: 123, name: "Rey"} # # @api public module SymbolizeKeys # Hook for being included in a class # # @api private # @return [void] # @raise [ArgumentError] when the base class isn't a Mash def self.included(base) raise ArgumentError, "#{base} must descent from Hashie::Mash" unless base <= Hashie::Mash end private # Converts a key to a symbol, if possible # # @api private # @param [] key the key to attempt convert to a symbol # @return [Symbol, K] def convert_key(key) key.respond_to?(:to_sym) ? key.to_sym : key end end end end end hashie-5.0.0/lib/hashie/extensions/merge_initializer.rb000066400000000000000000000013511414227716500232230ustar00rootroot00000000000000module Hashie module Extensions # The MergeInitializer is a super-simple mixin that allows # you to initialize a subclass of Hash with another Hash # to give you faster startup time for Hash subclasses. Note # that you can still provide a default value as a second # argument to the initializer. # # @example # class MyHash < Hash # include Hashie::Extensions::MergeInitializer # end # # h = MyHash.new(:abc => 'def') # h[:abc] # => 'def' # module MergeInitializer def initialize(hash = {}, default = nil, &block) default ? super(default) : super(&block) hash.each do |key, value| self[key] = value end end end end end hashie-5.0.0/lib/hashie/extensions/method_access.rb000066400000000000000000000170361414227716500223310ustar00rootroot00000000000000module Hashie module Extensions # MethodReader allows you to access keys of the hash # via method calls. This gives you an OStruct like way # to access your hash's keys. It will recognize keys # either as strings or symbols. # # Note that while nil keys will be returned as nil, # undefined keys will raise NoMethodErrors. Also note that # #respond_to? has been patched to appropriately recognize # key methods. # # @example # class User < Hash # include Hashie::Extensions::MethodReader # end # # user = User.new # user['first_name'] = 'Michael' # user.first_name # => 'Michael' # # user[:last_name] = 'Bleigh' # user.last_name # => 'Bleigh' # # user[:birthday] = nil # user.birthday # => nil # # user.not_declared # => NoMethodError module MethodReader def respond_to_missing?(name, include_private = false) return true if key?(name.to_s) || key?(name.to_sym) super end def method_missing(name, *args) if key?(name) self[name] else sname = name.to_s if key?(sname) self[sname] elsif sname[-1] == '?' kname = sname[0..-2] key?(kname) || key?(kname.to_sym) else super end end end end # MethodWriter gives you #key_name= shortcuts for # writing to your hash. Keys are written as strings, # override #convert_key if you would like to have symbols # or something else. # # Note that MethodWriter also overrides #respond_to such # that any #method_name= will respond appropriately as true. # # @example # class MyHash < Hash # include Hashie::Extensions::MethodWriter # end # # h = MyHash.new # h.awesome = 'sauce' # h['awesome'] # => 'sauce' # module MethodWriter def respond_to_missing?(name, include_private = false) return true if name.to_s =~ /=$/ super end def method_missing(name, *args) if args.size == 1 && name.to_s =~ /(.*)=$/ return self[convert_key(Regexp.last_match[1])] = args.first end super end def convert_key(key) key.to_s end end # MethodQuery gives you the ability to check for the truthiness # of a key via method calls. Note that it will return false if # the key is set to a non-truthful value, not if the key isn't # set at all. Use #key? for checking if a key has been set. # # MethodQuery will check against both string and symbol names # of the method for existing keys. It also patches #respond_to # to appropriately detect the query methods. # # @example # class MyHash < Hash # include Hashie::Extensions::MethodQuery # end # # h = MyHash.new # h['abc'] = 123 # h.abc? # => true # h['def'] = nil # h.def? # => false # h.hji? # => NoMethodError module MethodQuery def respond_to_missing?(name, include_private = false) if query_method?(name) && indifferent_key?(key_from_query_method(name)) true else super end end def method_missing(name, *args) return super unless args.empty? if query_method?(name) key = key_from_query_method(name) if indifferent_key?(key) !!(self[key] || self[key.to_sym]) else super end else super end end private def indifferent_key?(name) name = name.to_s key?(name) || key?(name.to_sym) end def key_from_query_method(query_method) query_method.to_s[0..-2] end def query_method?(name) name.to_s.end_with?('?') end end # A macro module that will automatically include MethodReader, # MethodWriter, and MethodQuery, giving you the ability to read, # write, and query keys in a hash using method call shortcuts. module MethodAccess def self.included(base) [MethodReader, MethodWriter, MethodQuery].each do |mod| base.send :include, mod end end end # A module shared between MethodOverridingWriter and MethodOverridingInitializer # to contained shared logic. This module aids in redefining existing hash methods. module RedefineMethod protected def method?(name) methods.map(&:to_s).include?(name) end def redefine_method(method_name) eigenclass = class << self; self; end eigenclass.__send__(:alias_method, "__#{method_name}", method_name) eigenclass.__send__(:define_method, method_name, -> { self[method_name] }) end end # MethodOverridingWriter gives you #key_name= shortcuts for # writing to your hash. It allows methods to be overridden by # #key_name= shortcuts and aliases those methods with two # leading underscores. # # Keys are written as strings. Override #convert_key if you # would like to have symbols or something else. # # Note that MethodOverridingWriter also overrides # #respond_to_missing? such that any #method_name= will respond # appropriately as true. # # @example # class MyHash < Hash # include Hashie::Extensions::MethodOverridingWriter # end # # h = MyHash.new # h.awesome = 'sauce' # h['awesome'] # => 'sauce' # h.zip = 'a-dee-doo-dah' # h.zip # => 'a-dee-doo-dah' # h.__zip # => [[['awesome', 'sauce'], ['zip', 'a-dee-doo-dah']]] # module MethodOverridingWriter include RedefineMethod def convert_key(key) key.to_s end def method_missing(name, *args) if args.size == 1 && name.to_s =~ /(.*)=$/ key = Regexp.last_match[1] redefine_method(key) if method?(key) && !already_overridden?(key) return self[convert_key(key)] = args.first end super end def respond_to_missing?(name, include_private = false) return true if name.to_s.end_with?('=') super end protected def already_overridden?(name) method?("__#{name}") end end # A macro module that will automatically include MethodReader, # MethodOverridingWriter, and MethodQuery, giving you the ability # to read, write, and query keys in a hash using method call # shortcuts that can override object methods. Any overridden # object method is automatically aliased with two leading # underscores. module MethodAccessWithOverride def self.included(base) [MethodReader, MethodOverridingWriter, MethodQuery, MethodOverridingInitializer].each do |mod| base.send :include, mod end end end # MethodOverridingInitializer allows you to override default hash # methods when passing in values from an existing hash. The overriden # methods are aliased with two leading underscores. # # @example # class MyHash < Hash # include Hashie::Extensions::MethodOverridingInitializer # end # # h = MyHash.new(zip: 'a-dee-doo-dah') # h.zip # => 'a-dee-doo-dah' # h.__zip # => [[['zip', 'a-dee-doo-dah']]] module MethodOverridingInitializer include RedefineMethod def initialize(hash = {}) hash.each do |key, value| skey = key.to_s redefine_method(skey) if method?(skey) self[skey] = value end end end end end hashie-5.0.0/lib/hashie/extensions/parsers/000077500000000000000000000000001414227716500206535ustar00rootroot00000000000000hashie-5.0.0/lib/hashie/extensions/parsers/yaml_erb_parser.rb000066400000000000000000000027231414227716500243520ustar00rootroot00000000000000require 'yaml' require 'erb' require 'pathname' module Hashie module Extensions module Parsers class YamlErbParser def initialize(file_path, options = {}) @content = File.read(file_path) @file_path = file_path.is_a?(Pathname) ? file_path.to_s : file_path @options = options end def perform template = ERB.new(@content) template.filename = @file_path permitted_classes = @options.fetch(:permitted_classes) { [] } permitted_symbols = @options.fetch(:permitted_symbols) { [] } aliases = @options.fetch(:aliases) { true } yaml_safe_load(template, permitted_classes, permitted_symbols, aliases) end def self.perform(file_path, options = {}) new(file_path, options).perform end private if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0') # Ruby 2.6+ def yaml_safe_load(template, permitted_classes, permitted_symbols, aliases) YAML.safe_load( template.result, permitted_classes: permitted_classes, permitted_symbols: permitted_symbols, aliases: aliases ) end else def yaml_safe_load(template, permitted_classes, permitted_symbols, aliases) YAML.safe_load(template.result, permitted_classes, permitted_symbols, aliases) end end end end end end hashie-5.0.0/lib/hashie/extensions/pretty_inspect.rb000066400000000000000000000006631414227716500226020ustar00rootroot00000000000000module Hashie module Extensions module PrettyInspect def self.included(base) base.send :alias_method, :hash_inspect, :inspect base.send :alias_method, :inspect, :hashie_inspect end def hashie_inspect ret = "#<#{self.class}" keys.sort_by(&:to_s).each do |key| ret << " #{key}=#{self[key].inspect}" end ret << '>' ret end end end end hashie-5.0.0/lib/hashie/extensions/ruby_version.rb000066400000000000000000000032131414227716500222460ustar00rootroot00000000000000# Copyright (c) Chad Fowler, Rich Kilmer, Jim Weirich and others. # Portions copyright (c) Engine Yard and Andre Arko # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # 'Software'), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. module Hashie module Extensions class RubyVersion include Comparable attr_accessor :segments def initialize(version) @segments = split_to_segments(version) end def <=>(other) lhsegments = segments rhsegments = other.segments lhsize = lhsegments.size rhsize = rhsegments.size limit = (lhsize > rhsize ? lhsize : rhsize) - 1 i = 0 while i <= limit lhs = lhsegments[i] || 0 rhs = rhsegments[i] || 0 i += 1 next if lhs == rhs return -1 if lhs.is_a?(String) && rhs.is_a?(Numeric) return 1 if lhs.is_a?(Numeric) && rhs.is_a?(String) return lhs <=> rhs end 0 end private def split_to_segments(version) version.scan(/[0-9]+|[a-z]+/i).map do |segment| /^\d+$/ =~ segment ? segment.to_i : segment end.freeze end end end end hashie-5.0.0/lib/hashie/extensions/ruby_version_check.rb000066400000000000000000000007031414227716500234040ustar00rootroot00000000000000require 'hashie/extensions/ruby_version' module Hashie module Extensions module RubyVersionCheck def self.included(base) base.extend ClassMethods end module ClassMethods def with_minimum_ruby(version) yield if with_minimum_ruby?(version) end def with_minimum_ruby?(version) RubyVersion.new(RUBY_VERSION) >= RubyVersion.new(version) end end end end end hashie-5.0.0/lib/hashie/extensions/strict_key_access.rb000066400000000000000000000035031414227716500232230ustar00rootroot00000000000000module Hashie module Extensions # SRP: This extension will fail an error whenever a key is accessed # that does not exist in the hash. # # EXAMPLE: # # class StrictKeyAccessHash < Hash # include Hashie::Extensions::StrictKeyAccess # end # # >> hash = StrictKeyAccessHash[foo: "bar"] # => {:foo=>"bar"} # >> hash[:foo] # => "bar" # >> hash[:cow] # KeyError: key not found: :cow # # NOTE: For googlers coming from Python to Ruby, this extension makes a Hash # behave more like a "Dictionary". # module StrictKeyAccess class DefaultError < StandardError def initialize super('Setting or using a default with Hashie::Extensions::StrictKeyAccess'\ ' does not make sense' ) end end # NOTE: Defaults don't make any sense with a StrictKeyAccess. # NOTE: When key lookup fails a KeyError is raised. # # Normal: # # >> a = Hash.new(123) # => {} # >> a["noes"] # => 123 # # With StrictKeyAccess: # # >> a = StrictKeyAccessHash.new(123) # => {} # >> a["noes"] # KeyError: key not found: "noes" # def [](key) fetch(key) end def default(_ = nil) raise DefaultError end def default=(_) raise DefaultError end def default_proc raise DefaultError end def default_proc=(_) raise DefaultError end def key(value) super.tap do |result| if result.nil? && (!key?(result) || self[result] != value) raise KeyError, "key not found with value of #{value.inspect}" end end end end end end hashie-5.0.0/lib/hashie/extensions/stringify_keys.rb000066400000000000000000000036441414227716500226010ustar00rootroot00000000000000module Hashie module Extensions module StringifyKeys # Convert all keys in the hash to strings. # # @example # test = {:abc => 'def'} # test.stringify_keys! # test # => {'abc' => 'def'} def stringify_keys! StringifyKeys.stringify_keys!(self) self end # Return a new hash with all keys converted # to strings. def stringify_keys StringifyKeys.stringify_keys(self) end module ClassMethods # Stringify all keys recursively within nested # hashes and arrays. # @api private def stringify_keys_recursively!(object) case object when self.class stringify_keys!(object) when ::Array object.each do |i| stringify_keys_recursively!(i) end when ::Hash stringify_keys!(object) end end # Convert all keys in the hash to strings. # # @param [::Hash] hash # @example # test = {:abc => 'def'} # test.stringify_keys! # test # => {'abc' => 'def'} def stringify_keys!(hash) hash.extend(Hashie::Extensions::StringifyKeys) unless hash.respond_to?(:stringify_keys!) hash.keys.each do |k| # rubocop:disable Performance/HashEachMethods stringify_keys_recursively!(hash[k]) hash[k.to_s] = hash.delete(k) end hash end # Return a copy of hash with all keys converted # to strings. # @param [::Hash] hash def stringify_keys(hash) copy = hash.dup copy.extend(Hashie::Extensions::StringifyKeys) unless copy.respond_to?(:stringify_keys!) copy.tap do |new_hash| stringify_keys!(new_hash) end end end class << self include ClassMethods end end end end hashie-5.0.0/lib/hashie/extensions/symbolize_keys.rb000066400000000000000000000043211414227716500225710ustar00rootroot00000000000000module Hashie module Extensions module SymbolizeKeys # Convert all keys in the hash to symbols. # # @example # test = {'abc' => 'def'} # test.symbolize_keys! # test # => {:abc => 'def'} def symbolize_keys! SymbolizeKeys.symbolize_keys!(self) self end # Return a new hash with all keys converted # to symbols. def symbolize_keys SymbolizeKeys.symbolize_keys(self) end module ClassMethods # Symbolize all keys recursively within nested # hashes and arrays. # @api private def symbolize_keys_recursively!(object) case object when self.class symbolize_keys!(object) when ::Array object.each do |i| symbolize_keys_recursively!(i) end when ::Hash symbolize_keys!(object) end end # Convert all keys in hash to symbols. # # @param [Hash] hash # @example # test = {'abc' => 'def'} # Hashie.symbolize_keys! test # test # => {:abc => 'def'} def symbolize_keys!(hash) hash.extend(Hashie::Extensions::SymbolizeKeys) unless hash.respond_to?(:symbolize_keys!) hash.keys.each do |k| # rubocop:disable Performance/HashEachMethods symbolize_keys_recursively!(hash[k]) hash[convert_key(k)] = hash.delete(k) end hash end # Return a copy of hash with all keys converted # to symbols. # @param [::Hash] hash def symbolize_keys(hash) copy = hash.dup copy.extend(Hashie::Extensions::SymbolizeKeys) unless copy.respond_to?(:symbolize_keys!) copy.tap do |new_hash| symbolize_keys!(new_hash) end end private # Converts a key to a symbol, if possible # # @api private # @param [] key the key to attempt convert to a symbol # @return [Symbol, K] def convert_key(key) key.respond_to?(:to_sym) ? key.to_sym : key end end class << self include ClassMethods end end end end hashie-5.0.0/lib/hashie/hash.rb000066400000000000000000000031661414227716500162530ustar00rootroot00000000000000require 'hashie/extensions/stringify_keys' require 'hashie/extensions/pretty_inspect' module Hashie # A Hashie Hash is simply a Hash that has convenience # functions baked in such as stringify_keys that may # not be available in all libraries. class Hash < ::Hash include Hashie::Extensions::PrettyInspect include Hashie::Extensions::StringifyKeys # Convert this hash into a Mash def to_mash ::Hashie::Mash.new(self) end # Converts a mash back to a hash (with stringified or symbolized keys) def to_hash(options = {}) out = {} each_key do |k| assignment_key = if options[:stringify_keys] k.to_s elsif options[:symbolize_keys] && k.respond_to?(:to_sym) k.to_sym else k end if self[k].is_a?(Array) out[assignment_key] ||= [] self[k].each do |array_object| out[assignment_key] << maybe_convert_to_hash(array_object, options) end else out[assignment_key] = maybe_convert_to_hash(self[k], options) end end out end # The C generator for the json gem doesn't like mashies def to_json(*args) to_hash.to_json(*args) end private def maybe_convert_to_hash(object, options) return object unless object.is_a?(Hash) || object.respond_to?(:to_hash) flexibly_convert_to_hash(object, options) end def flexibly_convert_to_hash(object, options = {}) if object.method(:to_hash).arity.zero? object.to_hash else object.to_hash(options) end end end end hashie-5.0.0/lib/hashie/logger.rb000066400000000000000000000005701414227716500166030ustar00rootroot00000000000000require 'logger' module Hashie # The logger that Hashie uses for reporting errors. # # @return [Logger] def self.logger @logger ||= Logger.new(STDOUT) end # Sets the logger that Hashie uses for reporting errors. # # @param logger [Logger] The logger to set as Hashie's logger. # @return [void] def self.logger=(logger) @logger = logger end end hashie-5.0.0/lib/hashie/mash.rb000066400000000000000000000277301414227716500162630ustar00rootroot00000000000000require 'hashie/hash' require 'hashie/array' require 'hashie/utils' require 'hashie/logger' require 'hashie/extensions/key_conflict_warning' module Hashie # Mash allows you to create pseudo-objects that have method-like # accessors for hash keys. This is useful for such implementations # as an API-accessing library that wants to fake robust objects # without the overhead of actually doing so. Think of it as OpenStruct # with some additional goodies. # # A Mash will look at the methods you pass it and perform operations # based on the following rules: # # * No punctuation: Returns the value of the hash for that key, or nil if none exists. # * Assignment (=): Sets the attribute of the given method name. # * Truthiness (?): Returns true or false depending on the truthiness of # the attribute, or false if the key is not set. # * Bang (!): Forces the existence of this key, used for deep Mashes. Think of it # as "touch" for mashes. # * Under Bang (_): Like Bang, but returns a new Mash rather than creating a key. # Used to test existance in deep Mashes. # # == Basic Example # # mash = Mash.new # mash.name? # => false # mash.name = "Bob" # mash.name # => "Bob" # mash.name? # => true # # == Hash Conversion Example # # hash = {:a => {:b => 23, :d => {:e => "abc"}}, :f => [{:g => 44, :h => 29}, 12]} # mash = Mash.new(hash) # mash.a.b # => 23 # mash.a.d.e # => "abc" # mash.f.first.g # => 44 # mash.f.last # => 12 # # == Bang Example # # mash = Mash.new # mash.author # => nil # mash.author! # => # # mash = Mash.new # mash.author!.name = "Michael Bleigh" # mash.author # => # # == Under Bang Example # # mash = Mash.new # mash.author # => nil # mash.author_ # => # mash.author_.name # => nil # # mash = Mash.new # mash.author_.name = "Michael Bleigh" (assigned to temp object) # mash.author # => # class Mash < Hash include Hashie::Extensions::RubyVersionCheck extend Hashie::Extensions::KeyConflictWarning ALLOWED_SUFFIXES = %w[? ! = _].freeze def self.load(path, options = {}) @_mashes ||= new return @_mashes[path] if @_mashes.key?(path) raise ArgumentError, "The following file doesn't exist: #{path}" unless File.file?(path) options = options.dup parser = options.delete(:parser) { Hashie::Extensions::Parsers::YamlErbParser } @_mashes[path] = new(parser.perform(path, options)).freeze end def to_module(mash_method_name = :settings) mash = self Module.new do |m| m.send :define_method, mash_method_name.to_sym do mash end end end def with_accessors! extend Hashie::Extensions::Mash::DefineAccessors end alias to_s inspect # If you pass in an existing hash, it will # convert it to a Mash including recursively # descending into arrays and hashes, converting # them as well. def initialize(source_hash = nil, default = nil, &blk) deep_update(source_hash) if source_hash default ? super(default) : super(&blk) end # Creates a new anonymous subclass with key conflict # warnings disabled. You may pass an array of method # symbols to restrict the disabled warnings to. # Hashie::Mash.quiet.new(hash) all warnings disabled. # Hashie::Mash.quiet(:zip).new(hash) only zip warning # is disabled. def self.quiet(*method_keys) @memoized_classes ||= {} @memoized_classes[method_keys] ||= Class.new(self) do disable_warnings(*method_keys) end end class << self; alias [] new; end alias regular_reader [] alias regular_writer []= # Retrieves an attribute set in the Mash. Will convert a key passed in # as a symbol to a string before retrieving. def custom_reader(key) default_proc.call(self, key) if default_proc && !key?(key) value = regular_reader(convert_key(key)) yield value if block_given? value end # Sets an attribute in the Mash. Symbol keys will be converted to # strings before being set, and Hashes will be converted into Mashes # for nesting purposes. def custom_writer(key, value, convert = true) #:nodoc: log_built_in_message(key) if key.respond_to?(:to_sym) && log_collision?(key.to_sym) regular_writer(convert_key(key), convert ? convert_value(value) : value) end alias [] custom_reader alias []= custom_writer # This is the bang method reader, it will return a new Mash # if there isn't a value already assigned to the key requested. def initializing_reader(key) ck = convert_key(key) regular_writer(ck, self.class.new) unless key?(ck) regular_reader(ck) end # This is the under bang method reader, it will return a temporary new Mash # if there isn't a value already assigned to the key requested. def underbang_reader(key) ck = convert_key(key) if key?(ck) regular_reader(ck) else self.class.new end end def fetch(key, *args) super(convert_key(key), *args) end def delete(key) super(convert_key(key)) end def values_at(*keys) super(*keys.map { |key| convert_key(key) }) end # Returns a new instance of the class it was called on, using its keys as # values, and its values as keys. The new values and keys will always be # strings. def invert self.class.new(super) end # Returns a new instance of the class it was called on, containing elements # for which the given block returns false. def reject(&blk) self.class.new(super(&blk)) end # Returns a new instance of the class it was called on, containing elements # for which the given block returns true. def select(&blk) self.class.new(super(&blk)) end alias regular_dup dup # Duplicates the current mash as a new mash. def dup self.class.new(self, default, &default_proc) end alias regular_key? key? def key?(key) super(convert_key(key)) end alias has_key? key? alias include? key? alias member? key? if with_minimum_ruby?('2.6.0') # Performs a deep_update on a duplicate of the # current mash. def deep_merge(*other_hashes, &blk) dup.deep_update(*other_hashes, &blk) end # Recursively merges this mash with the passed # in hash, merging each hash in the hierarchy. def deep_update(*other_hashes, &blk) other_hashes.each do |other_hash| _deep_update(other_hash, &blk) end self end else # Performs a deep_update on a duplicate of the # current mash. def deep_merge(other_hash, &blk) dup.deep_update(other_hash, &blk) end # Recursively merges this mash with the passed # in hash, merging each hash in the hierarchy. def deep_update(other_hash, &blk) _deep_update(other_hash, &blk) self end end # Alias these lexically so they get the correctly defined # #deep_merge and #deep_update based on ruby version. alias merge deep_merge alias deep_merge! deep_update alias update deep_update alias merge! update def _deep_update(other_hash, &blk) other_hash.each_pair do |k, v| key = convert_key(k) if v.is_a?(::Hash) && key?(key) && regular_reader(key).is_a?(Mash) custom_reader(key).deep_update(v, &blk) else value = convert_value(v, true) value = convert_value(yield(key, self[k], value), true) if blk && key?(k) custom_writer(key, value, false) end end end private :_deep_update # Assigns a value to a key def assign_property(name, value) self[name] = value end # Performs a shallow_update on a duplicate of the current mash def shallow_merge(other_hash) dup.shallow_update(other_hash) end # Merges (non-recursively) the hash from the argument, # changing the receiving hash def shallow_update(other_hash) other_hash.each_pair do |k, v| regular_writer(convert_key(k), convert_value(v, true)) end self end def replace(other_hash) (keys - other_hash.keys).each { |key| delete(key) } other_hash.each { |key, value| self[key] = value } self end def respond_to_missing?(method_name, *args) return true if key?(method_name) suffix = method_suffix(method_name) if suffix true else super end end def prefix_method?(method_name) method_name = method_name.to_s method_name.end_with?(*ALLOWED_SUFFIXES) && key?(method_name.chop) end def method_missing(method_name, *args, &blk) # rubocop:disable Style/MethodMissing return self.[](method_name, &blk) if key?(method_name) name, suffix = method_name_and_suffix(method_name) case suffix when '='.freeze assign_property(name, args.first) when '?'.freeze !!self[name] when '!'.freeze initializing_reader(name) when '_'.freeze underbang_reader(name) else self[method_name] end end # play nice with ActiveSupport Array#extract_options! def extractable_options? true end # another ActiveSupport method, see issue #270 def reverse_merge(other_hash) self.class.new(other_hash).merge(self) end with_minimum_ruby('2.3.0') do def dig(*keys) super(*keys.map { |key| convert_key(key) }) end end with_minimum_ruby('2.4.0') do def transform_values(&blk) self.class.new(super(&blk)) end # Returns a new instance of the class it was called on, with nil values # removed. def compact self.class.new(super) end end with_minimum_ruby('2.5.0') do def slice(*keys) string_keys = keys.map { |key| convert_key(key) } self.class.new(super(*string_keys)) end def transform_keys(&blk) self.class.new(super(&blk)) end end with_minimum_ruby('3.0.0') do def except(*keys) string_keys = keys.map { |key| convert_key(key) } self.class.new(super(*string_keys)) end end protected def method_name_and_suffix(method_name) method_name = method_name.to_s if method_name.end_with?(*ALLOWED_SUFFIXES) [method_name[0..-2], method_name[-1]] else [method_name[0..-1], nil] end end def method_suffix(method_name) method_name = method_name.to_s method_name[-1] if method_name.end_with?(*ALLOWED_SUFFIXES) end def convert_key(key) #:nodoc: key.respond_to?(:to_sym) ? key.to_s : key end def convert_value(val, duping = false) #:nodoc: case val when self.class val.dup when Hash duping ? val.dup : val when ::Hash val = val.dup if duping self.class.new(val) when ::Array Array.new(val.map { |e| convert_value(e) }) else val end end private def log_built_in_message(method_key) return if self.class.disable_warnings?(method_key) method_information = Hashie::Utils.method_information(method(method_key)) Hashie.logger.warn( 'You are setting a key that conflicts with a built-in method ' \ "#{self.class}##{method_key} #{method_information}. " \ 'This can cause unexpected behavior when accessing the key as a ' \ 'property. You can still access the key via the #[] method.' ) end def log_collision?(method_key) return unless method_key.is_a?(String) || method_key.is_a?(Symbol) return unless respond_to?(method_key) _, suffix = method_name_and_suffix(method_key) (!suffix || suffix == '='.freeze) && !self.class.disable_warnings?(method_key) && !(regular_key?(method_key) || regular_key?(method_key.to_s)) end end end hashie-5.0.0/lib/hashie/railtie.rb000066400000000000000000000012441414227716500167540ustar00rootroot00000000000000begin require 'rails/railtie' module Hashie class Railtie < Rails::Railtie # Set the Hashie.logger to use Rails.logger when used with rails. initializer 'hashie.configure_logger', after: 'initialize_logger' do Hashie.logger = Rails.logger end initializer 'hashie.patch_hash_except', after: 'load_active_support' do if Rails::VERSION::MAJOR >= 6 require 'hashie/extensions/active_support/core_ext/hash' Hashie::Mash.send(:include, Hashie::Extensions::ActiveSupport::CoreExt::Hash) end end end end rescue LoadError => e Hashie.logger.info("Hashie skipping railtie as #{e.message}") end hashie-5.0.0/lib/hashie/rash.rb000066400000000000000000000067201414227716500162640ustar00rootroot00000000000000module Hashie # # Rash is a Hash whose keys can be Regexps, or Ranges, which will # match many input keys. # # A good use case for this class is routing URLs in a web framework. # The Rash's keys match URL patterns, and the values specify actions # which can handle the URL. When the Rash's value is proc, the proc # will be automatically called with the regexp's matched groups as # block arguments. # # Usage example: # # greeting = Hashie::Rash.new( /^Mr./ => "Hello sir!", /^Mrs./ => "Evening, madame." ) # greeting["Mr. Steve Austin"] #=> "Hello sir!" # greeting["Mrs. Steve Austin"] #=> "Evening, madame." # # Note: The Rash is automatically optimized every 500 accesses # (Regexps get sorted by how often they get matched). # If this is too low or too high, you can tune it by # setting: `rash.optimize_every = n` # class Rash attr_accessor :optimize_every def initialize(initial = {}) @hash = {} @regexes = [] @ranges = [] @regex_counts = Hash.new(0) @optimize_every = 500 @lookups = 0 update(initial) end def update(other) other.each do |key, value| self[key] = value end self end def []=(key, value) case key when Regexp # key = normalize_regex(key) # this used to just do: /#{regexp}/ @regexes << key when Range @ranges << key end @hash[key] = value end # # Return the first thing that matches the key. # def [](key) all(key).first end # # Raise (or yield) unless something matches the key. # def fetch(*args) raise ArgumentError, "Expected 1-2 arguments, got #{args.length}" \ unless (1..2).cover?(args.length) key, default = args all(key) do |value| return value end if block_given? yield key elsif default default else raise KeyError, "key not found: #{key.inspect}" end end # # Return everything that matches the query. # def all(query) return to_enum(:all, query) unless block_given? if @hash.include? query yield @hash[query] return end case query when String optimize_if_necessary! # see if any of the regexps match the string @regexes.each do |regex| match = regex.match(query) next unless match @regex_counts[regex] += 1 value = @hash[regex] if value.respond_to? :call yield value.call(match) else yield value end end when Numeric # see if any of the ranges match the integer @ranges.each do |range| yield @hash[range] if range.cover? query end when Regexp # Reverse operation: `rash[/regexp/]` returns all string keys matching the regexp @hash.each do |key, val| yield val if key.is_a?(String) && query =~ key end end end def method_missing(*args, &block) @hash.send(*args, &block) || super end def respond_to_missing?(method_name, _include_private = false) @hash.respond_to?(method_name) end private def optimize_if_necessary! return unless (@lookups += 1) >= @optimize_every @regexes = @regexes.sort_by { |regex| -@regex_counts[regex] } @lookups = 0 end end end hashie-5.0.0/lib/hashie/trash.rb000066400000000000000000000006721414227716500164500ustar00rootroot00000000000000require 'hashie/dash' require 'hashie/extensions/dash/property_translation' module Hashie # A Trash is a 'translated' Dash where the keys can be remapped from a source # hash. # # Trashes are useful when you need to read data from another application, # such as a Java api, where the keys are named differently from how we would # in Ruby. class Trash < Dash include Hashie::Extensions::Dash::PropertyTranslation end end hashie-5.0.0/lib/hashie/utils.rb000066400000000000000000000022361414227716500164650ustar00rootroot00000000000000module Hashie # A collection of helper methods that can be used throughout the gem. module Utils # Describes a method by where it was defined. # # @param bound_method [Method] The method to describe. # @return [String] def self.method_information(bound_method) if bound_method.source_location "defined at #{bound_method.source_location.join(':')}" else "defined in #{bound_method.owner}" end end # Duplicates a value or returns the value when it is not duplicable # # @api public # # @param value [Object] the value to safely duplicate # @return [Object] the duplicated value def self.safe_dup(value) case value when Complex, FalseClass, NilClass, Rational, Method, Symbol, TrueClass, *integer_classes value else value.dup end end # Lists the classes Ruby uses for integers # # @api private # @return [Array] def self.integer_classes @integer_classes ||= if 0.class == Integer [Integer] else [Fixnum, Bignum] # rubocop:disable Lint/UnifiedInteger end end end end hashie-5.0.0/lib/hashie/version.rb000066400000000000000000000000551414227716500170070ustar00rootroot00000000000000module Hashie VERSION = '5.0.0'.freeze end hashie-5.0.0/mascot.svg000066400000000000000000000766661414227716500150220ustar00rootroot00000000000000 eierlegende-Wollmilchsau hashie-5.0.0/spec/000077500000000000000000000000001414227716500137205ustar00rootroot00000000000000hashie-5.0.0/spec/fixtures/000077500000000000000000000000001414227716500155715ustar00rootroot00000000000000hashie-5.0.0/spec/fixtures/yaml_with_aliases.yml000066400000000000000000000002571414227716500220160ustar00rootroot00000000000000accounts: &base_accounts admin: password: secret company_a: accounts: &accounts <<: *base_accounts # company_b: # accounts: &accounts # <<: *base_accounts hashie-5.0.0/spec/fixtures/yaml_with_symbols.yml000066400000000000000000000000511414227716500220550ustar00rootroot00000000000000:user_icon: :width: 200 :height: 200 hashie-5.0.0/spec/hashie/000077500000000000000000000000001414227716500151615ustar00rootroot00000000000000hashie-5.0.0/spec/hashie/array_spec.rb000066400000000000000000000013301414227716500176330ustar00rootroot00000000000000require 'spec_helper' describe Array do with_minimum_ruby('2.3.0') do describe '#dig' do let(:array) { Hashie::Array.new(%i[a b c]) } it 'works with a string index' do expect(array.dig('0')).to eq(:a) end it 'works with a numeric index' do expect(array.dig(1)).to eq(:b) end context 'when array is empty' do let(:array) { Hashie::Array.new([]) } it 'works with a first numeric and next string index' do expect(array.dig(0, 'hello')).to eq(nil) end it 'throws an error with first string and next numeric index' do expect { array.dig('hello', 0) }.to raise_error(TypeError) end end end end end hashie-5.0.0/spec/hashie/clash_spec.rb000066400000000000000000000040621414227716500176140ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Clash do it 'is able to set an attribute via method_missing' do subject.foo('bar') expect(subject[:foo]).to eq 'bar' end it 'is able to set multiple attributes' do subject.foo('bar').baz('wok') expect(subject).to eq(foo: 'bar', baz: 'wok') end it 'converts multiple arguments into an array' do subject.foo(1, 2, 3) expect(subject[:foo]).to eq [1, 2, 3] end it 'is able to use bang notation to create a new Clash on a key' do subject.foo! expect(subject[:foo]).to be_kind_of(Hashie::Clash) end it 'is able to chain onto the new Clash when using bang notation' do subject.foo!.bar('abc').baz(123) expect(subject).to eq(foo: { bar: 'abc', baz: 123 }) end it 'is able to jump back up to the parent in the chain with #_end!' do subject.foo!.bar('abc')._end!.baz(123) expect(subject).to eq(foo: { bar: 'abc' }, baz: 123) end it 'merges rather than replaces existing keys' do subject.where(abc: 'def').where(hgi: 123) expect(subject).to eq(where: { abc: 'def', hgi: 123 }) end it 'is able to replace all of its own keys with #replace' do subject.foo(:bar).hello(:world) expect(subject.replace(baz: 123, hgi: 123)).to eq(baz: 123, hgi: 123) expect(subject).to eq(baz: 123, hgi: 123) expect(subject[:foo]).to be_nil expect(subject[:hello]).to be_nil end it 'merges multiple bang notation calls' do subject.where!.foo(123) subject.where!.bar(321) expect(subject).to eq(where: { foo: 123, bar: 321 }) end it 'raises an exception when method is missing' do expect { subject.boo }.to raise_error(NoMethodError) end describe 'when inherited' do subject { Class.new(described_class).new } it 'bang nodes are instances of a subclass' do subject.where!.foo(123) expect(subject[:where]).to be_instance_of(subject.class) end it 'merged nodes are instances of a subclass' do subject.where(abc: 'def').where(hgi: 123) expect(subject[:where]).to be_instance_of(subject.class) end end end hashie-5.0.0/spec/hashie/dash_spec.rb000066400000000000000000000522621414227716500174460ustar00rootroot00000000000000require 'spec_helper' Hashie::Hash.class_eval do def self.inherited(klass) klass.instance_variable_set('@inheritance_test', true) end end class DashTest < Hashie::Dash property :first_name, required: true property :email property :count, default: 0 end class DashTestDefaultProc < Hashie::Dash property :fields, default: -> { [] } end class DashNoRequiredTest < Hashie::Dash property :first_name property :email property :count, default: 0 end class DashWithCoercion < Hashie::Dash include Hashie::Extensions::Coercion property :person property :city coerce_key :person, ::DashNoRequiredTest end class PropertyBangTest < Hashie::Dash property :important! end class SubclassedTest < DashTest property :last_name, required: true end class RequiredMessageTest < DashTest property :first_name, required: true, message: 'must be set.' end class DashDefaultTest < Hashie::Dash property :aliases, default: ['Snake'] end class DeferredTest < Hashie::Dash property :created_at, default: proc { Time.now } end class DeferredWithSelfTest < Hashie::Dash property :created_at, default: -> { Time.now } property :updated_at, default: ->(test) { test.created_at } end describe DashTestDefaultProc do it 'to_json behaves correctly with default proc' do object = described_class.new expect(object.to_json).to be == '{"fields":[]}' end end describe DashTest do def property_required_error(property) [ArgumentError, "The property '#{property}' is required for #{subject.class.name}."] end def property_required_custom_error(property) [ArgumentError, "The property '#{property}' must be set."] end def property_message_without_required_error [ArgumentError, 'The :message option should be used with :required option.'] end def no_property_error(property) [NoMethodError, "The property '#{property}' is not defined for #{subject.class.name}."] end subject { DashTest.new(first_name: 'Bob', email: 'bob@example.com') } let(:required_message) { RequiredMessageTest.new(first_name: 'Bob') } it('subclasses Hashie::Hash') { should respond_to(:to_mash) } describe '#to_s' do subject { super().to_s } it { should eq '#' } end it 'lists all set properties in inspect' do subject.first_name = 'Bob' subject.email = 'bob@example.com' expect(subject.inspect).to eq '#' end describe '#count' do subject { super().count } it { should be_zero } end it { should respond_to(:first_name) } it { should respond_to(:first_name=) } it { should_not respond_to(:nonexistent) } it 'errors out for a non-existent property' do expect { subject['nonexistent'] }.to raise_error(*no_property_error('nonexistent')) end it 'errors out when attempting to set a required property to nil' do expect { subject.first_name = nil }.to raise_error(*property_required_error('first_name')) end it 'errors out when message added to not required property' do expect do class DashMessageOptionWithoutRequiredTest < Hashie::Dash property :first_name, message: 'is required.' end end.to raise_error(*property_message_without_required_error) expect do class DashMessageOptionWithoutRequiredTest < Hashie::Dash property :first_name, required: false, message: 'is required.' end end.to raise_error(*property_message_without_required_error) end context 'writing to properties' do it 'fails writing a required property to nil' do expect { subject.first_name = nil }.to raise_error(*property_required_error('first_name')) expect { required_message.first_name = nil } .to raise_error(*property_required_custom_error('first_name')) end it 'fails writing a required property to nil using []=' do expect { subject[:first_name] = nil }.to raise_error(*property_required_error('first_name')) expect { required_message[:first_name] = nil } .to raise_error(*property_required_custom_error('first_name')) end it 'fails writing to a non-existent property using []=' do expect { subject['nonexistent'] = 123 }.to raise_error(*no_property_error('nonexistent')) end it 'works for an existing property using []=' do subject[:first_name] = 'Bob' expect(subject[:first_name]).to eq 'Bob' expect { subject['first_name'] }.to raise_error(*no_property_error('first_name')) end it 'works for an existing property using a method call' do subject.first_name = 'Franklin' expect(subject.first_name).to eq 'Franklin' end end context 'reading from properties' do it 'fails reading from a non-existent property using []' do expect { subject['nonexistent'] }.to raise_error(*no_property_error('nonexistent')) end it 'is able to retrieve properties through blocks' do subject[:first_name] = 'Aiden' value = nil subject.[](:first_name) { |v| value = v } expect(value).to eq 'Aiden' end it 'is able to retrieve properties through blocks with method calls' do subject[:first_name] = 'Frodo' value = nil subject.first_name { |v| value = v } expect(value).to eq 'Frodo' end end context 'reading from deferred properties' do it 'evaluates proc after initial read' do expect(DeferredTest.new[:created_at]).to be_instance_of(Time) end it 'does not evalute proc after subsequent reads' do deferred = DeferredTest.new expect(deferred[:created_at].object_id).to eq deferred[:created_at].object_id end end context 'reading from a deferred property based on context' do it 'provides the current hash as context for evaluation' do deferred = DeferredWithSelfTest.new expect(deferred[:created_at].object_id).to eq deferred[:created_at].object_id expect(deferred[:updated_at].object_id).to eq deferred[:created_at].object_id end end context 'converting from a Mash' do class ConvertingFromMash < Hashie::Dash property :property, required: true end context 'without keeping the original keys' do let(:mash) { Hashie::Mash.new(property: 'test') } it 'does not pick up the property from the stringified key' do expect { ConvertingFromMash.new(mash) }.to raise_error(NoMethodError) end end context 'when keeping the original keys' do class KeepingMash < Hashie::Mash include Hashie::Extensions::Mash::KeepOriginalKeys end let(:mash) { KeepingMash.new(property: 'test') } it 'picks up the property from the original key' do expect { ConvertingFromMash.new(mash) }.not_to raise_error end end end describe '#new' do it 'fails with non-existent properties' do expect { described_class.new(bork: '') }.to raise_error(*no_property_error('bork')) end it 'sets properties that it is able to' do obj = described_class.new first_name: 'Michael' expect(obj.first_name).to eq 'Michael' end it 'accepts nil' do expect { DashNoRequiredTest.new(nil) }.not_to raise_error end it 'accepts block to define a global default' do obj = described_class.new { |_, key| key.to_s.upcase } expect(obj.first_name).to eq 'FIRST_NAME' expect(obj.count).to be_zero end it 'fails when required values are missing' do expect { DashTest.new }.to raise_error(*property_required_error('first_name')) end it 'does not overwrite default values' do obj1 = DashDefaultTest.new obj1.aliases << 'El Rey' obj2 = DashDefaultTest.new expect(obj2.aliases).not_to include 'El Rey' end end describe '#merge' do it 'creates a new instance of the Dash' do new_dash = subject.merge(first_name: 'Robert') expect(subject.object_id).not_to eq new_dash.object_id end it 'merges the given hash' do new_dash = subject.merge(first_name: 'Robert', email: 'robert@example.com') expect(new_dash.first_name).to eq 'Robert' expect(new_dash.email).to eq 'robert@example.com' end it 'fails with non-existent properties' do expect { subject.merge(middle_name: 'James') } .to raise_error(*no_property_error('middle_name')) end it 'errors out when attempting to set a required property to nil' do expect { subject.merge(first_name: nil) } .to raise_error(*property_required_error('first_name')) end context 'given a block' do it "sets merged key's values to the block's return value" do expect(subject.merge(first_name: 'Jim') do |key, oldval, newval| "#{key}: #{newval} #{oldval}" end.first_name).to eq 'first_name: Jim Bob' end end end describe '#merge!' do it 'modifies the existing instance of the Dash' do original_dash = subject.merge!(first_name: 'Robert') expect(subject.object_id).to eq original_dash.object_id end it 'merges the given hash' do subject.merge!(first_name: 'Robert', email: 'robert@example.com') expect(subject.first_name).to eq 'Robert' expect(subject.email).to eq 'robert@example.com' end it 'fails with non-existent properties' do expect { subject.merge!(middle_name: 'James') }.to raise_error(NoMethodError) end it 'errors out when attempting to set a required property to nil' do expect { subject.merge!(first_name: nil) }.to raise_error(ArgumentError) end context 'given a block' do it "sets merged key's values to the block's return value" do expect(subject.merge!(first_name: 'Jim') do |key, oldval, newval| "#{key}: #{newval} #{oldval}" end.first_name).to eq 'first_name: Jim Bob' end end end describe 'properties' do it 'lists defined properties' do expect(described_class.properties).to eq Set.new(%i[first_name email count]) end it 'checks if a property exists' do expect(described_class.property?(:first_name)).to be_truthy expect(described_class.property?('first_name')).to be_falsy end it 'checks if a property is required' do expect(described_class.required?(:first_name)).to be_truthy expect(described_class.required?('first_name')).to be_falsy end it 'doesnt include property from subclass' do expect(described_class.property?(:last_name)).to be_falsy end it 'lists declared defaults' do expect(described_class.defaults).to eq(count: 0) end it 'allows properties that end in bang' do expect(PropertyBangTest.property?(:important!)).to be_truthy end end describe '#replace' do before { subject.replace(first_name: 'Cain') } it 'return self' do expect(subject.replace(email: 'bar').object_id).to eq subject.object_id end it 'sets all specified keys to their corresponding values' do expect(subject.first_name).to eq 'Cain' end it 'leaves only specified keys and keys with default values' do expect(subject.keys.sort_by(&:to_s)).to eq %i[count first_name] expect(subject.email).to be_nil expect(subject.count).to eq 0 end context 'when replacing keys with default values' do before { subject.replace(count: 3) } it 'sets all specified keys to their corresponding values' do expect(subject.count).to eq 3 end end end describe '#update_attributes!(params)' do let(:params) { { first_name: 'Alice', email: 'alice@example.com' } } context 'when there is coercion' do let(:params_before) do { city: 'nyc', person: { first_name: 'Bob', email: 'bob@example.com' } } end let(:params_after) do { city: 'sfo', person: { first_name: 'Alice', email: 'alice@example.com' } } end subject { DashWithCoercion.new(params_before) } it 'update the attributes' do expect(subject.person.first_name).to eq params_before[:person][:first_name] subject.update_attributes!(params_after) expect(subject.person.first_name).to eq params_after[:person][:first_name] end end it 'update the attributes' do subject.update_attributes!(params) expect(subject.first_name).to eq params[:first_name] expect(subject.email).to eq params[:email] expect(subject.count).to eq subject.class.defaults[:count] end context 'when required property is update to nil' do let(:params) { { first_name: nil, email: 'alice@example.com' } } it 'raise an ArgumentError' do expect { subject.update_attributes!(params) }.to raise_error(ArgumentError) end end context 'when a default property is update to nil' do let(:params) { { count: nil, email: 'alice@example.com' } } it 'set the property back to the default value' do subject.update_attributes!(params) expect(subject.email).to eq params[:email] expect(subject.count).to eq subject.class.defaults[:count] end end context 'codependent attributes' do let(:codependent) do Class.new(Hashie::Dash) do property :a, required: -> { b.nil? }, message: 'is required if b is not set.' property :b, required: -> { a.nil? }, message: 'is required if a is not set.' property :c, default: -> { 'c' } end end it 'does not raise an error when only the first property is set' do expect { codependent.new(a: 'ant', b: nil) }.not_to raise_error end it 'does not raise an error when only the second property is set' do expect { codependent.new(a: nil, b: 'bat') }.not_to raise_error end it 'does not raise an error when both properties are set' do expect { codependent.new(a: 'ant', b: 'bat') }.not_to raise_error end it 'raises an error when neither property is set' do expect { codependent.new(a: nil, b: nil) }.to raise_error(ArgumentError) end context 'exporting nil values' do describe '#to_h' do it 'does not prune nil values' do expect(codependent.new(a: 'hi', b: nil).to_h).to eq(a: 'hi', b: nil, c: 'c') expect(codependent.new(a: 'hi', b: nil, c: nil).to_hash).to eq(a: 'hi', b: nil, c: 'c') expect(codependent.new(a: 'hi', b: nil).merge(c: nil).to_h).to( eq(a: 'hi', b: nil, c: nil) ) end end describe '#to_hash' do it 'does not prune nil values' do expect(codependent.new(a: 'hi', b: nil).to_hash).to eq(a: 'hi', b: nil, c: 'c') expect(codependent.new(a: 'hi', b: nil, c: nil).to_hash).to eq(a: 'hi', b: nil, c: 'c') expect(codependent.new(a: 'hi', b: nil).merge(c: nil).to_hash).to( eq(a: 'hi', b: nil, c: nil) ) end end describe '**' do # Note: This test is an implementation detail of MRI and may not hold for # other Ruby interpreters. But it's important to note in the test suite # because it can be surprising for people unfamiliar with the semantics of # double-splatting. # # For more information, see [this link][1]: # # [1]: https://github.com/hashie/hashie/issues/353#issuecomment-363294886 it 'prunes nil values because they are not set in the dash' do dash = codependent.new(a: 'hi', b: nil) expect(**dash).to eq(a: 'hi', c: 'c') end end end end end end describe Hashie::Dash, 'inheritance' do before do @top = Class.new(Hashie::Dash) @middle = Class.new(@top) @bottom = Class.new(@middle) end it 'reports empty properties when nothing defined' do expect(@top.properties).to be_empty expect(@top.defaults).to be_empty end it 'inherits properties downwards' do @top.property :echo expect(@middle.properties).to include(:echo) expect(@bottom.properties).to include(:echo) end it 'doesnt inherit properties upwards' do @middle.property :echo expect(@top.properties).not_to include(:echo) expect(@bottom.properties).to include(:echo) end it 'allows overriding a default on an existing property' do @top.property :echo @middle.property :echo, default: 123 expect(@bottom.properties.to_a).to eq [:echo] expect(@bottom.new.echo).to eq 123 end it 'allows clearing an existing default' do @top.property :echo @middle.property :echo, default: 123 @bottom.property :echo expect(@bottom.properties.to_a).to eq [:echo] expect(@bottom.new.echo).to be_nil end it 'allows nil defaults' do @bottom.property :echo, default: nil expect(@bottom.new).to have_key(:echo) expect(@bottom.new).to_not have_key('echo') end context 'exporting nil values' do let(:test) do Class.new(Hashie::Dash) do property :foo property :bar end end describe '#to_h' do it 'does not prune nil values' do expect(test.new(foo: 'hi', bar: nil).to_h).to eq(foo: 'hi', bar: nil) end end describe '#to_hash' do it 'does not prune nil values' do expect(test.new(foo: 'hi', bar: nil).to_hash).to eq(foo: 'hi', bar: nil) end end describe '**' do # Note: This test is an implementation detail of MRI and may not hold for # other Ruby interpreters. But it's important to note in the test suite # because it can be surprising for people unfamiliar with the semantics of # double-splatting. # # For more information, see [this link][1]: # # [1]: https://github.com/hashie/hashie/issues/353#issuecomment-363294886 it 'prunes nil values because they are not set in the dash' do dash = test.new(foo: 'hi', bar: nil) expect(**dash).to eq(foo: 'hi') end end end end describe SubclassedTest do subject { SubclassedTest.new(first_name: 'Bob', last_name: 'McNob', email: 'bob@example.com') } describe '#count' do subject { super().count } it { should be_zero } end it { should respond_to(:first_name) } it { should respond_to(:first_name=) } it { should respond_to(:last_name) } it { should respond_to(:last_name=) } it 'has one additional property' do expect(described_class.property?(:last_name)).to be_truthy end it "didn't override superclass inheritance logic" do expect(described_class.instance_variable_get('@inheritance_test')).to be_truthy end end class ConditionallyRequiredTest < Hashie::Dash property :username property :password, required: -> { !username.nil? }, message: 'must be set, too.' end describe ConditionallyRequiredTest do it 'does not allow a conditionally required property to be set to nil if required' do expect { ConditionallyRequiredTest.new(username: 'bob.smith', password: nil) } .to raise_error(ArgumentError, "The property 'password' must be set, too.") end it 'allows a conditionally required property to be set to nil if not required' do expect { ConditionallyRequiredTest.new(username: nil, password: nil) }.not_to raise_error end it 'allows a conditionally required property to be set if required' do expect { ConditionallyRequiredTest.new(username: 'bob.smith', password: '$ecure!') } .not_to raise_error end end class MixedPropertiesTest < Hashie::Dash property :symbol property 'string' end describe MixedPropertiesTest do subject { MixedPropertiesTest.new('string' => 'string', symbol: 'symbol') } it { should respond_to('string') } it { should respond_to(:symbol) } it 'property?' do expect(described_class.property?('string')).to be_truthy expect(described_class.property?(:symbol)).to be_truthy end it 'fetch' do expect(subject['string']).to eq('string') expect { subject[:string] }.to raise_error(NoMethodError) expect(subject[:symbol]).to eq('symbol') expect { subject['symbol'] }.to raise_error(NoMethodError) end it 'double define' do klass = Class.new(MixedPropertiesTest) do property 'symbol' end instance = klass.new(symbol: 'one', 'symbol' => 'two') expect(instance[:symbol]).to eq('one') expect(instance['symbol']).to eq('two') end it 'assign' do subject['string'] = 'updated' expect(subject['string']).to eq('updated') expect { subject[:string] = 'updated' }.to raise_error(NoMethodError) subject[:symbol] = 'updated' expect(subject[:symbol]).to eq('updated') expect { subject['symbol'] = 'updated' }.to raise_error(NoMethodError) end end context 'Dynamic Dash Class' do it 'define property' do klass = Class.new(Hashie::Dash) my_property = 'my_property' my_orig = my_property.dup klass.property(my_property) expect(my_property).to eq(my_orig) end end context 'with method access' do class DashWithMethodAccess < Hashie::Dash include Hashie::Extensions::IndifferentAccess include Hashie::Extensions::MethodQuery property :test end subject(:dash) { DashWithMethodAccess.new(test: 'value') } describe '#test' do subject { dash.test } it { is_expected.to eq('value') } end describe '#test?' do subject { dash.test? } it { is_expected.to eq true } end end RSpec.describe Hashie::Dash do let(:test) do Class.new(Hashie::Dash) do property :description, default: '' end end include_examples 'Dash default handling', :description end hashie-5.0.0/spec/hashie/extensions/000077500000000000000000000000001414227716500173605ustar00rootroot00000000000000hashie-5.0.0/spec/hashie/extensions/autoload_spec.rb000066400000000000000000000021341414227716500225270ustar00rootroot00000000000000require 'spec_helper' require 'hashie' describe Hashie::Extensions do describe 'autloads constants' do it { is_expected.to be_const_defined(:MethodAccess) } it { is_expected.to be_const_defined(:Coercion) } it { is_expected.to be_const_defined(:DeepMerge) } it { is_expected.to be_const_defined(:IgnoreUndeclared) } it { is_expected.to be_const_defined(:IndifferentAccess) } it { is_expected.to be_const_defined(:MergeInitializer) } it { is_expected.to be_const_defined(:MethodAccess) } it { is_expected.to be_const_defined(:MethodQuery) } it { is_expected.to be_const_defined(:MethodReader) } it { is_expected.to be_const_defined(:MethodWriter) } it { is_expected.to be_const_defined(:StringifyKeys) } it { is_expected.to be_const_defined(:SymbolizeKeys) } it { is_expected.to be_const_defined(:DeepFetch) } it { is_expected.to be_const_defined(:DeepFind) } it { is_expected.to be_const_defined(:PrettyInspect) } it { is_expected.to be_const_defined(:KeyConversion) } it { is_expected.to be_const_defined(:MethodAccessWithOverride) } end end hashie-5.0.0/spec/hashie/extensions/coercion_spec.rb000066400000000000000000000454341414227716500225320ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::Coercion do class NotInitializable private_class_method :new end class Initializable attr_reader :coerced, :value def initialize(obj, coerced = nil) @coerced = coerced @value = obj.class.to_s end def coerced? !@coerced.nil? end end class Coercable < Initializable def self.coerce(obj) new(obj, true) end end before(:each) do class ExampleCoercableHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer end end subject { ExampleCoercableHash } let(:instance) { subject.new } describe '#coerce_key' do context 'nesting' do class BaseCoercableHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer end class NestedCoercableHash < BaseCoercableHash coerce_key :foo, String coerce_key :bar, Integer end class OtherNestedCoercableHash < BaseCoercableHash coerce_key :foo, Symbol end class RootCoercableHash < BaseCoercableHash coerce_key :nested, NestedCoercableHash coerce_key :other, OtherNestedCoercableHash coerce_key :nested_list, Array[NestedCoercableHash] coerce_key :nested_hash, Hash[String => NestedCoercableHash] end def test_nested_object(obj) expect(obj).to be_a(NestedCoercableHash) expect(obj[:foo]).to be_a(String) expect(obj[:bar]).to be_an(Integer) end subject { RootCoercableHash } let(:instance) { subject.new } it 'does not add coercions to superclass' do instance[:nested] = { foo: 'bar' } instance[:other] = { foo: 'bar' } expect(instance[:nested][:foo]).to be_a String expect(instance[:other][:foo]).to be_a Symbol end it 'coerces nested objects' do instance[:nested] = { foo: 123, bar: '456' } test_nested_object(instance[:nested]) end it 'coerces nested arrays' do instance[:nested_list] = [ { foo: 123, bar: '456' }, { foo: 234, bar: '567' }, { foo: 345, bar: '678' } ] expect(instance[:nested_list]).to be_a Array expect(instance[:nested_list].size).to eq(3) instance[:nested_list].each do |nested| test_nested_object nested end end it 'coerces nested hashes' do instance[:nested_hash] = { a: { foo: 123, bar: '456' }, b: { foo: 234, bar: '567' }, c: { foo: 345, bar: '678' } } expect(instance[:nested_hash]).to be_a Hash expect(instance[:nested_hash].size).to eq(3) instance[:nested_hash].each do |key, nested| expect(key).to be_a(String) test_nested_object nested end end context 'when repetitively including the module' do class RepetitiveCoercableHash < NestedCoercableHash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer coerce_key :nested, NestedCoercableHash end subject { RepetitiveCoercableHash } let(:instance) { subject.new } it 'does not raise a stack overflow error' do expect do instance[:nested] = { foo: 123, bar: '456' } test_nested_object(instance[:nested]) end.not_to raise_error end end end it { expect(subject).to be_respond_to(:coerce_key) } it 'runs through coerce on a specified key' do subject.coerce_key :foo, Coercable instance[:foo] = 'bar' expect(instance[:foo]).to be_coerced end it 'skips unnecessary coercions' do subject.coerce_key :foo, Coercable instance[:foo] = Coercable.new('bar') expect(instance[:foo]).to_not be_coerced end it 'supports an array of keys' do subject.coerce_keys :foo, :bar, Coercable instance[:foo] = 'bar' instance[:bar] = 'bax' expect(instance[:foo]).to be_coerced expect(instance[:bar]).to be_coerced end it 'supports coercion for Array' do subject.coerce_key :foo, Array[Coercable] instance[:foo] = %w[bar bar2] expect(instance[:foo]).to all(be_coerced) expect(instance[:foo]).to be_a(Array) end it 'supports coercion for Set' do subject.coerce_key :foo, Set[Coercable] instance[:foo] = Set.new(%w[bar bar2]) expect(instance[:foo]).to all(be_coerced) expect(instance[:foo]).to be_a(Set) end it 'supports coercion for Set of primitive' do subject.coerce_key :foo, Set[Initializable] instance[:foo] = %w[bar bar2] expect(instance[:foo].map(&:value)).to all(eq 'String') expect(instance[:foo]).to be_none(&:coerced?) expect(instance[:foo]).to be_a(Set) end it 'supports coercion for Hash' do subject.coerce_key :foo, Hash[Coercable => Coercable] instance[:foo] = { 'bar_key' => 'bar_value', 'bar2_key' => 'bar2_value' } expect(instance[:foo].keys).to all(be_coerced) expect(instance[:foo].values).to all(be_coerced) expect(instance[:foo]).to be_a(Hash) end it 'supports coercion for Hash with primitive as value' do subject.coerce_key :foo, Hash[Coercable => Initializable] instance[:foo] = { 'bar_key' => '1', 'bar2_key' => '2' } expect(instance[:foo].values.map(&:value)).to all(eq 'String') expect(instance[:foo].keys).to all(be_coerced) end context 'coercing core types' do def test_coercion(literal, target_type, coerce_method) subject.coerce_key :foo, target_type instance[:foo] = literal expect(instance[:foo]).to be_a(target_type) expect(instance[:foo]).to eq(literal.send(coerce_method)) end RSpec.shared_examples 'coerces from numeric types' do |target_type, coerce_method| it "coerces from String to #{target_type} via #{coerce_method}" do test_coercion '2.0', target_type, coerce_method end it "coerces from Integer to #{target_type} via #{coerce_method}" do # Fixnum test_coercion 2, target_type, coerce_method # Bignum test_coercion 12_345_667_890_987_654_321, target_type, coerce_method end it "coerces from Rational to #{target_type} via #{coerce_method}" do test_coercion Rational(2, 3), target_type, coerce_method end end RSpec.shared_examples 'coerces from alphabetical types' do |target_type, coerce_method| it "coerces from String to #{target_type} via #{coerce_method}" do test_coercion 'abc', target_type, coerce_method end it "coerces from Symbol to #{target_type} via #{coerce_method}" do test_coercion :abc, target_type, coerce_method end end include_examples 'coerces from numeric types', Integer, :to_i include_examples 'coerces from numeric types', Float, :to_f include_examples 'coerces from numeric types', String, :to_s include_examples 'coerces from alphabetical types', String, :to_s include_examples 'coerces from alphabetical types', Symbol, :to_sym it 'can coerce String to Rational when possible' do test_coercion '2/3', Rational, :to_r end it 'can coerce String to Complex when possible' do test_coercion '2/3+3/4i', Complex, :to_c end it 'coerces collections with core types' do subject.coerce_key :foo, Hash[String => String] instance[:foo] = { abc: 123, xyz: 987 } expect(instance[:foo]).to eq( 'abc' => '123', 'xyz' => '987' ) end it 'can coerce via a proc' do subject.coerce_key(:foo, lambda do |v| case v when String return !!(v =~ /^(true|t|yes|y|1)$/i) when Numeric return !v.to_i.zero? else return v == true end end) true_values = [true, 'true', 't', 'yes', 'y', '1', 1, -1] false_values = [false, 'false', 'f', 'no', 'n', '0', 0] true_values.each do |v| instance[:foo] = v expect(instance[:foo]).to be_a(TrueClass) end false_values.each do |v| instance[:foo] = v expect(instance[:foo]).to be_a(FalseClass) end end it 'raises errors for non-coercable types' do subject.coerce_key :foo, NotInitializable expect { instance[:foo] = 'true' } .to raise_error(Hashie::CoercionError, /NotInitializable is not a coercable type/) end it 'can coerce false' do subject.coerce_key :foo, Coercable instance[:foo] = false expect(instance[:foo]).to be_coerced expect(instance[:foo].value).to eq('FalseClass') end it 'does not coerce nil' do subject.coerce_key :foo, String instance[:foo] = nil expect(instance[:foo]).to_not eq('') expect(instance[:foo]).to be_nil end end it 'calls #new if no coerce method is available' do subject.coerce_key :foo, Initializable instance[:foo] = 'bar' expect(instance[:foo].value).to eq 'String' expect(instance[:foo]).not_to be_coerced end it 'coerces when the merge initializer is used' do subject.coerce_key :foo, Coercable instance = subject.new(foo: 'bar') expect(instance[:foo]).to be_coerced end context 'when #replace is used' do before { subject.coerce_key :foo, :bar, Coercable } let(:instance) do subject.new(foo: 'bar').replace(foo: 'foz', bar: 'baz', hi: 'bye') end it 'coerces relevant keys' do expect(instance[:foo]).to be_coerced expect(instance[:bar]).to be_coerced expect(instance[:hi]).not_to respond_to(:coerced?) end it 'sets correct values' do expect(instance[:hi]).to eq 'bye' end end context 'when used with a Mash' do class UserMash < Hashie::Mash end class TweetMash < Hashie::Mash include Hashie::Extensions::Coercion coerce_key :user, UserMash end it 'coerces with instance initialization' do tweet = TweetMash.new(user: { email: 'foo@bar.com' }) expect(tweet[:user]).to be_a(UserMash) end it 'coerces when setting with attribute style' do tweet = TweetMash.new tweet.user = { email: 'foo@bar.com' } expect(tweet[:user]).to be_a(UserMash) end it 'coerces when setting with string index' do tweet = TweetMash.new tweet['user'] = { email: 'foo@bar.com' } expect(tweet[:user]).to be_a(UserMash) end it 'coerces when setting with symbol index' do tweet = TweetMash.new tweet[:user] = { email: 'foo@bar.com' } expect(tweet[:user]).to be_a(UserMash) end end context 'when used with a Trash' do class UserTrash < Hashie::Trash property :email end class TweetTrash < Hashie::Trash include Hashie::Extensions::Coercion property :user, from: :user_data coerce_key :user, UserTrash end it 'coerces with instance initialization' do tweet = TweetTrash.new(user_data: { email: 'foo@bar.com' }) expect(tweet[:user]).to be_a(UserTrash) end end context 'when used with IndifferentAccess to coerce a Mash' do class MyHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::IndifferentAccess include Hashie::Extensions::MergeInitializer end class UserHash < MyHash end class TweetHash < MyHash coerce_key :user, UserHash end it 'coerces with instance initialization' do tweet = TweetHash.new(user: Hashie::Mash.new(email: 'foo@bar.com')) expect(tweet[:user]).to be_a(UserHash) end it 'coerces when setting with string index' do tweet = TweetHash.new tweet['user'] = Hashie::Mash.new(email: 'foo@bar.com') expect(tweet[:user]).to be_a(UserHash) end it 'coerces when setting with symbol index' do tweet = TweetHash.new tweet[:user] = Hashie::Mash.new(email: 'foo@bar.com') expect(tweet[:user]).to be_a(UserHash) end end context 'when subclassing' do class MyOwnBase < Hash include Hashie::Extensions::Coercion end class MyOwnHash < MyOwnBase coerce_key :value, Integer end class MyOwnSubclass < MyOwnHash end it 'inherits key coercions' do expect(MyOwnHash.key_coercions).to eql(MyOwnSubclass.key_coercions) end it 'the superclass does not accumulate coerced attributes from subclasses' do expect(MyOwnBase.key_coercions).to eq({}) end end context 'when using circular coercion' do context 'with a proc on one side' do class CategoryHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer coerce_key :products, lambda { |value| return value.map { |v| ProductHash.new(v) } if value.respond_to?(:map) ProductHash.new(v) } end class ProductHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer coerce_key :categories, Array[CategoryHash] end let(:category) do CategoryHash.new(type: 'rubygem', products: [Hashie::Mash.new(name: 'Hashie')]) end let(:product) do ProductHash.new(name: 'Hashie', categories: [Hashie::Mash.new(type: 'rubygem')]) end it 'coerces CategoryHash[:products] correctly' do expected = [ProductHash] actual = category[:products].map(&:class) expect(actual).to eq(expected) end it 'coerces ProductHash[:categories] correctly' do expected = [CategoryHash] actual = product[:categories].map(&:class) expect(actual).to eq(expected) end end context 'without a proc on either side' do it 'fails with a NameError since the other class is not defined yet' do attempted_code = lambda do class AnotherCategoryHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer coerce_key :products, Array[AnotherProductHash] end class AnotherProductHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer coerce_key :categories, Array[AnotherCategoryHash] end end expect { attempted_code.call }.to raise_error(NameError) end end end end describe '#coerce_value' do context 'with strict: true' do it 'coerces any value of the exact right class' do subject.coerce_value String, Coercable instance[:foo] = 'bar' instance[:bar] = 'bax' instance[:hi] = :bye expect(instance[:foo]).to be_coerced expect(instance[:bar]).to be_coerced expect(instance[:hi]).not_to respond_to(:coerced?) end it 'coerces values from a #replace call' do subject.coerce_value String, Coercable instance[:foo] = :bar instance.replace(foo: 'bar', bar: 'bax') expect(instance[:foo]).to be_coerced expect(instance[:bar]).to be_coerced end it 'does not coerce superclasses' do klass = Class.new(String) subject.coerce_value klass, Coercable instance[:foo] = 'bar' expect(instance[:foo]).not_to be_kind_of(Coercable) instance[:foo] = klass.new expect(instance[:foo]).to be_kind_of(Coercable) end end context 'core types' do it 'coerces String to Integer when possible' do subject.coerce_value String, Integer instance[:foo] = '2' instance[:bar] = '2.7' instance[:hi] = 'hi' expect(instance[:foo]).to be_a(Integer) expect(instance[:foo]).to eq(2) expect(instance[:bar]).to be_a(Integer) expect(instance[:bar]).to eq(2) expect(instance[:hi]).to be_a(Integer) expect(instance[:hi]).to eq(0) # not what I expected... end it 'coerces non-numeric from String to Integer' do # This was surprising, but I guess it's "correct" # unless there is a stricter `to_i` alternative subject.coerce_value String, Integer instance[:hi] = 'hi' expect(instance[:hi]).to be_a(Integer) expect(instance[:hi]).to eq(0) end it 'raises a CoercionError when coercion is not possible' do type = if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >= Hashie::Extensions::RubyVersion.new('2.4.0') Integer else Fixnum end subject.coerce_value type, Symbol expect { instance[:hi] = 1 }.to raise_error( Hashie::CoercionError, /Cannot coerce property :hi from #{type} to Symbol/ ) end it 'coerces Integer to String' do type = if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >= Hashie::Extensions::RubyVersion.new('2.4.0') Integer else Fixnum end subject.coerce_value type, String { fixnum: 2, bignum: 12_345_667_890_987_654_321, float: 2.7, rational: Rational(2, 3), complex: Complex(1) }.each do |k, v| instance[k] = v if v.is_a? type expect(instance[k]).to be_a(String) expect(instance[k]).to eq(v.to_s) else expect(instance[k]).to_not be_a(String) expect(instance[k]).to eq(v) end end end it 'coerces Numeric to String' do subject.coerce_value Numeric, String { fixnum: 2, bignum: 12_345_667_890_987_654_321, float: 2.7, rational: Rational(2, 3), complex: Complex(1) }.each do |k, v| instance[k] = v expect(instance[k]).to be_a(String) expect(instance[k]).to eq(v.to_s) end end it 'can coerce via a proc' do subject.coerce_value(String, lambda do |v| return !!(v =~ /^(true|t|yes|y|1)$/i) end) true_values = %w[true t yes y 1] false_values = %w[false f no n 0] true_values.each do |v| instance[:foo] = v expect(instance[:foo]).to be_a(TrueClass) end false_values.each do |v| instance[:foo] = v expect(instance[:foo]).to be_a(FalseClass) end end end end after(:each) do Object.send(:remove_const, :ExampleCoercableHash) end end hashie-5.0.0/spec/hashie/extensions/dash/000077500000000000000000000000001414227716500202775ustar00rootroot00000000000000hashie-5.0.0/spec/hashie/extensions/dash/coercion_spec.rb000066400000000000000000000005101414227716500234330ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::Dash::Coercion do class DashWithCoercion < Hashie::Dash include Hashie::Extensions::Dash::Coercion property :type, coerce: Symbol end it 'does the coercion of properties' do expect(DashWithCoercion.new(type: 'something')).to eq(type: :something) end end hashie-5.0.0/spec/hashie/extensions/dash/indifferent_access_spec.rb000066400000000000000000000057361414227716500254670ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::Dash::IndifferentAccess do class TrashWithIndifferentAccess < Hashie::Trash include Hashie::Extensions::Dash::IndifferentAccess property :per_page, transform_with: ->(v) { v.to_i } property :total, from: :total_pages end class DashWithIndifferentAccess < Hashie::Dash include Hashie::Extensions::Dash::IndifferentAccess property :name end context 'when included in Trash' do let(:params) { { per_page: '1', total_pages: 2 } } subject { TrashWithIndifferentAccess.new(params) } it 'gets the expected behaviour' do expect(subject.per_page).to eq params[:per_page].to_i expect(subject.total).to eq params[:total_pages] end end context 'when included in Dash' do let(:patch) { Hashie::Extensions::Dash::IndifferentAccess::ClassMethods } let(:dash_class) { Class.new(Hashie::Dash) } it 'extends with the patch once' do expect(patch).to receive(:extended).with(dash_class).once dash_class.send(:include, Hashie::Extensions::Dash::IndifferentAccess) end end context 'initialized with' do it 'string' do instance = DashWithIndifferentAccess.new('name' => 'Name') expect(instance.name).to eq('Name') expect(instance['name']).to eq('Name') expect(instance[:name]).to eq('Name') expect(instance.inspect).to eq('#') expect(instance.to_hash).to eq('name' => 'Name') end it 'key' do instance = DashWithIndifferentAccess.new(name: 'Name') expect(instance.name).to eq('Name') expect(instance['name']).to eq('Name') expect(instance[:name]).to eq('Name') expect(instance.inspect).to eq('#') expect(instance.to_hash).to eq('name' => 'Name') end end it 'updates' do instance = DashWithIndifferentAccess.new instance['name'] = 'Updated String' expect(instance.name).to eq('Updated String') instance[:name] = 'Updated Symbol' expect(instance.name).to eq('Updated Symbol') instance.name = 'Updated Method' expect(instance.name).to eq('Updated Method') end context 'initialized with both prefers last assignment' do it 'string, then symbol' do instance = DashWithIndifferentAccess.new('name' => 'First', name: 'Last') expect(instance.name).to eq('Last') expect(instance['name']).to eq('Last') expect(instance[:name]).to eq('Last') expect(instance.inspect).to eq('#') expect(instance.to_hash).to eq('name' => 'Last') end it 'symbol then string' do instance = DashWithIndifferentAccess.new(name: 'Last', 'name' => 'First') expect(instance.name).to eq('First') expect(instance['name']).to eq('First') expect(instance[:name]).to eq('First') expect(instance.inspect).to eq('#') expect(instance.to_hash).to eq('name' => 'First') end end end hashie-5.0.0/spec/hashie/extensions/dash/predefined_values_spec.rb000066400000000000000000000032271414227716500253260ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::Dash::PredefinedValues do let(:extended_dash) do Class.new(Hashie::Dash) do include Hashie::Extensions::Dash::PredefinedValues property :gender, values: %i[male female prefer_not_to_say] property :age, values: (0..150) end end it 'allows value within the predefined list' do valid_dash = extended_dash.new(gender: :male) expect(valid_dash.gender).to eq(:male) end it 'rejects value outside the predefined list' do expect { extended_dash.new(gender: :unicorn) } .to raise_error(ArgumentError, %(Invalid value for property 'gender')) end it 'accepts a range for predefined list' do expect { extended_dash.new(age: -1) } .to raise_error(ArgumentError, %(Invalid value for property 'age')) end it 'allows property to be nil' do expect { extended_dash.new } .not_to raise_error end it 'rejects non array or range for predefined list' do expect do class DashWithUnsupportedValueType < Hashie::Dash include Hashie::Extensions::Dash::PredefinedValues property :name, values: -> { :foo } end end.to raise_error(ArgumentError, %(`values` accepts an Array or a Range.)) end let(:subclass) do Class.new(extended_dash) do property :language, values: %i[ruby javascript] end end it 'passes property predefined list to subclasses' do expect { subclass.new(gender: :unicorn) } .to raise_error(ArgumentError, %(Invalid value for property 'gender')) end it 'allows subclass to define predefined list' do expect { subclass.new(language: :ruby) } .not_to raise_error end end hashie-5.0.0/spec/hashie/extensions/deep_fetch_spec.rb000066400000000000000000000061251414227716500230110ustar00rootroot00000000000000require 'spec_helper' module Hashie module Extensions describe DeepFetch do subject { Class.new(Hash) { include Hashie::Extensions::DeepFetch } } let(:hash) do { library: { books: [ { title: 'Call of the Wild' }, { title: 'Moby Dick' } ], shelves: nil, location: { address: '123 Library St.' } } } end let(:instance) { subject.new.update(hash) } describe '#deep_fetch' do it 'extracts a value from a nested hash' do expect(instance.deep_fetch(:library, :location, :address)).to eq('123 Library St.') end it 'extracts a value from a nested array' do expect(instance.deep_fetch(:library, :books, 1, :title)).to eq('Moby Dick') end context 'when one of the keys is not present' do context 'when a block is provided' do it 'returns the value of the block' do value = instance.deep_fetch(:library, :unknown_key, :location) { 'block value' } expect(value).to eq('block value') end end context 'when a block is not provided' do context 'when the nested object is an array' do it 'raises an UndefinedPathError' do expect do instance.deep_fetch(:library, :books, 2) end.to( raise_error( DeepFetch::UndefinedPathError, 'Could not fetch path (library > books > 2) at 2' ) ) end end context 'when the nested object is a hash' do it 'raises a UndefinedPathError' do expect do instance.deep_fetch(:library, :location, :unknown_key) end.to( raise_error( DeepFetch::UndefinedPathError, 'Could not fetch path (library > location > unknown_key) at unknown_key' ) ) end end context 'when the nested object is missing' do it 'raises an UndefinedPathError' do expect do instance.deep_fetch(:library, :unknown_key, :books) end.to( raise_error( DeepFetch::UndefinedPathError, 'Could not fetch path (library > unknown_key > books) at unknown_key' ) ) end end context 'when the nested object is nil' do it 'raises an UndefinedPathError' do expect do instance.deep_fetch(:library, :shelves, :address) end.to( raise_error( DeepFetch::UndefinedPathError, 'Could not fetch path (library > shelves > address) at address' ) ) end end end end end end end end hashie-5.0.0/spec/hashie/extensions/deep_find_spec.rb000066400000000000000000000063311414227716500226370ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::DeepFind do subject { Class.new(Hash) { include Hashie::Extensions::DeepFind } } let(:hash) do { library: { books: [ { title: 'Call of the Wild' }, { title: 'Moby Dick' } ], shelves: nil, location: { address: '123 Library St.', title: 'Main Library' } } } end let(:instance) { subject.new.update(hash) } describe '#deep_find' do it 'detects a value from a nested hash' do expect(instance.deep_find(:address)).to eq('123 Library St.') end it 'detects a value from a nested array' do expect(instance.deep_find(:title)).to eq('Call of the Wild') end it 'returns nil if it does not find a match' do expect(instance.deep_find(:wahoo)).to be_nil end end describe '#deep_find_all' do it 'detects all values from a nested hash' do expect(instance.deep_find_all(:title)) .to eq(['Call of the Wild', 'Moby Dick', 'Main Library']) end it 'returns nil if it does not find any matches' do expect(instance.deep_find_all(:wahoo)).to be_nil end context 'when match value is hash itself' do let(:hash) do { title: { type: :string }, library: { books: [ { title: 'Call of the Wild' }, { title: 'Moby Dick' } ], shelves: nil, location: { address: '123 Library St.', title: 'Main Library' } } } end it 'detects all values from a nested hash' do expect(instance.deep_find_all(:title)) .to eq([{ type: :string }, 'Call of the Wild', 'Moby Dick', 'Main Library']) end end end context 'on a Hash including Hashie::Extensions::IndifferentAccess' do let(:klass) { Class.new(Hash) { include Hashie::Extensions::IndifferentAccess } } subject(:instance) { klass[hash.dup].extend(Hashie::Extensions::DeepFind) } describe '#deep_find' do it 'indifferently detects a value from a nested hash' do expect(instance.deep_find(:address)).to eq('123 Library St.') expect(instance.deep_find('address')).to eq('123 Library St.') end it 'indifferently detects a value from a nested array' do expect(instance.deep_find(:title)).to eq('Call of the Wild') expect(instance.deep_find('title')).to eq('Call of the Wild') end it 'indifferently returns nil if it does not find a match' do expect(instance.deep_find(:wahoo)).to be_nil expect(instance.deep_find('wahoo')).to be_nil end end describe '#deep_find_all' do it 'indifferently detects all values from a nested hash' do expect(instance.deep_find_all(:title)) .to eq(['Call of the Wild', 'Moby Dick', 'Main Library']) expect(instance.deep_find_all('title')) .to eq(['Call of the Wild', 'Moby Dick', 'Main Library']) end it 'indifferently returns nil if it does not find any matches' do expect(instance.deep_find_all(:wahoo)).to be_nil expect(instance.deep_find_all('wahoo')).to be_nil end end end end hashie-5.0.0/spec/hashie/extensions/deep_locate_spec.rb000066400000000000000000000057011414227716500231660ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::DeepLocate do let(:hash) do { from: 0, size: 25, query: { bool: { must: [ { query_string: { query: 'foobar', default_operator: 'AND', fields: [ 'title^2', '_all' ] } }, { match: { field_1: 'value_1' } }, { range: { lsr09: { gte: 2014 } } } ], should: [ { match: { field_2: 'value_2' } } ], must_not: [ { range: { lsr10: { gte: 2014 } } } ] } } } end describe '.deep_locate' do context 'if called with a non-callable comparator' do it 'creates a key comparator on-th-fly' do expect(described_class.deep_locate(:lsr10, hash)) .to eq([hash[:query][:bool][:must_not][0][:range]]) end end it 'locates enumerables for which the given comparator returns true for at least one element' do examples = [ [ ->(key, _value, _object) { key == :fields }, [ hash[:query][:bool][:must].first[:query_string] ] ], [ ->(_key, value, _object) { value.is_a?(String) && value.include?('value') }, [ hash[:query][:bool][:must][1][:match], hash[:query][:bool][:should][0][:match] ] ], [ lambda do |_key, _value, object| object.is_a?(Array) && !object.extend(described_class).deep_locate(:match).empty? end, [ hash[:query][:bool][:must], hash[:query][:bool][:should] ] ] ] examples.each do |comparator, expected_result| expect(described_class.deep_locate(comparator, hash)).to eq(expected_result) end end it 'returns an empty array if nothing was found' do expect(described_class.deep_locate(:muff, foo: 'bar')).to eq([]) end end context 'if extending an existing object' do let(:extended_hash) do hash.extend(described_class) end it 'adds #deep_locate' do expect(extended_hash.deep_locate(:bool)).to eq([hash[:query]]) end end context 'if included in a hash' do let(:derived_hash_with_extension_included) do Class.new(Hash) do include Hashie::Extensions::DeepLocate end end let(:instance) do derived_hash_with_extension_included.new.update(hash) end it 'adds #deep_locate' do expect(instance.deep_locate(:bool)).to eq([hash[:query]]) end end end hashie-5.0.0/spec/hashie/extensions/deep_merge_spec.rb000066400000000000000000000060671414227716500230240ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::DeepMerge do class DeepMergeHash < Hash include Hashie::Extensions::DeepMerge end subject { DeepMergeHash } it 'should return initial hash for arguments that are not hash' do hash = subject.new.merge(a: 'a') expect(hash.deep_merge('abc')).to eq(hash) end context 'without &block' do let(:h1) do subject.new.merge( a: 'a', a1: 42, b: 'b', c: { c1: 'c1', c2: { a: 'b' }, c3: { d1: 'd1' } }, d: nil, d1: false, d2: true, d3: unbound_method, d4: Complex(1), d5: Rational(1) ) end let(:h2) { { a: 1, a1: 1, c: { c1: 2, c2: 'c2', c3: { d2: 'd2' } }, e: { e1: 1 } } } let(:unbound_method) { method(:puts) } let(:expected_hash) do { a: 1, a1: 1, b: 'b', c: { c1: 2, c2: 'c2', c3: { d1: 'd1', d2: 'd2' } }, d: nil, d1: false, d2: true, d3: unbound_method, d4: Complex(1), d5: Rational(1), e: { e1: 1 } } end it 'deep merges two hashes without modifying them' do result = h1.deep_merge(h2) expect(result).to eq expected_hash expect(h1).to( eq( a: 'a', a1: 42, b: 'b', c: { c1: 'c1', c2: { a: 'b' }, c3: { d1: 'd1' } }, d: nil, d1: false, d2: true, d3: unbound_method, d4: Complex(1), d5: Rational(1) ) ) expect(h2).to eq(a: 1, a1: 1, c: { c1: 2, c2: 'c2', c3: { d2: 'd2' } }, e: { e1: 1 }) end it 'deep merges another hash in place via bang method' do result = h1.deep_merge!(h2) expect(result).to eq expected_hash expect(h1).to eq expected_hash end it 'merges new nested hash entries by value, not by reference' do h1.deep_merge!(h2) expect { h1[:e][:e1] = 'changed' }.not_to(change { h2[:e][:e1] }) end end context 'with &block' do let(:h1) { subject.new.merge(a: 100, b: 200, c: { c1: 100 }) } let(:h2) { { b: 250, c: { c1: 200 } } } let(:expected_hash) { { a: 100, b: 450, c: { c1: 300 } } } let(:block) { proc { |_, this_val, other_val| this_val + other_val } } it 'deep merges two hashes' do expect(h1.deep_merge(h2, &block)).to eq expected_hash end it 'deep merges another hash in place via bang method' do h1.deep_merge!(h2, &block) expect(h1).to eq expected_hash end end context 'from extended object' do subject { Hash } let(:h1) { subject.new.merge(a: 100, c: { c1: 100 }).extend(Hashie::Extensions::DeepMerge) } let(:h2) { { b: 250, c: { c1: 200 } } } let(:expected_hash) { { a: 100, b: 250, c: { c1: 200 } } } it 'does not raise error' do expect { h1.deep_merge(h2) } .not_to raise_error end it 'deep merges two hashes' do expect(h1.deep_merge(h2)).to eq expected_hash end it 'deep merges another hash in place via bang method' do h1.deep_merge!(h2) expect(h1).to eq expected_hash end end end hashie-5.0.0/spec/hashie/extensions/ignore_undeclared_spec.rb000066400000000000000000000035601414227716500243740ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::IgnoreUndeclared do context 'included in Trash' do class ForgivingTrash < Hashie::Trash include Hashie::Extensions::IgnoreUndeclared property :city property :state, from: :provence property :str_state, from: 'str_provence' end subject { ForgivingTrash } it 'silently ignores undeclared properties on initialization' do expect { subject.new(city: 'Toronto', provence: 'ON', country: 'Canada') }.to_not raise_error end it 'works with translated properties (with symbol keys)' do expect(subject.new(provence: 'Ontario').state).to eq('Ontario') end it 'works with translated properties (with string keys)' do expect(subject.new('str_provence' => 'Ontario').str_state).to eq('Ontario') end it 'requires properties to be declared on assignment' do hash = subject.new(city: 'Toronto') expect { hash.country = 'Canada' }.to raise_error(NoMethodError) end context 'with a default value' do let(:test) do Class.new(Hashie::Trash) do include Hashie::Extensions::IgnoreUndeclared property :description, from: :desc, default: '' end end include_examples 'Dash default handling', :description, :desc end end context 'combined with DeepMerge' do class ForgivingTrashWithMerge < Hashie::Trash include Hashie::Extensions::DeepMerge include Hashie::Extensions::IgnoreUndeclared property :some_key end it 'deep merges' do class ForgivingTrashWithMergeAndProperty < ForgivingTrashWithMerge property :some_other_key end hash = ForgivingTrashWithMergeAndProperty.new(some_ignored_key: 17, some_key: 12) expect(hash.deep_merge(some_other_key: 55, some_ignored_key: 18)) .to eq(some_key: 12, some_other_key: 55) end end end hashie-5.0.0/spec/hashie/extensions/indifferent_access_spec.rb000066400000000000000000000247331414227716500245460ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::IndifferentAccess do class IndifferentHashWithMergeInitializer < Hash include Hashie::Extensions::MergeInitializer include Hashie::Extensions::IndifferentAccess class << self alias build new end end class IndifferentHashWithArrayInitializer < Hash include Hashie::Extensions::IndifferentAccess class << self alias build [] end end class IndifferentHashWithTryConvertInitializer < Hash include Hashie::Extensions::IndifferentAccess class << self alias build try_convert end end class IndifferentHashWithDash < Hashie::Dash include Hashie::Extensions::IndifferentAccess property :foo end class IndifferentHashWithIgnoreUndeclaredAndPropertyTranslation < Hashie::Dash include Hashie::Extensions::IgnoreUndeclared include Hashie::Extensions::Dash::PropertyTranslation include Hashie::Extensions::IndifferentAccess property :foo, from: :bar end describe '#merge' do it 'indifferently merges in a hash' do indifferent_hash = Class.new(::Hash) do include Hashie::Extensions::IndifferentAccess end.new merged_hash = indifferent_hash.merge(cat: 'meow') expect(merged_hash[:cat]).to eq('meow') expect(merged_hash['cat']).to eq('meow') end it 'injects the resulting new Hash with IndifferentAccess' do hash = IndifferentHashWithMergeInitializer.new( :cat => 'meow', 'dog' => { name: 'Mango', sound: 'woof' } ) dog = hash[:dog] merged = dog.merge(foo: 'bar') expect(merged[:foo]).to eq('bar') expect(merged['foo']).to eq('bar') end end describe '#merge!' do it 'indifferently merges in a hash' do indifferent_hash = Class.new(::Hash) do include Hashie::Extensions::IndifferentAccess end.new indifferent_hash[:cat] = 'meow' expect(indifferent_hash[:cat]).to eq('meow') expect(indifferent_hash['cat']).to eq('meow') end end describe '#to_hash' do let(:indifferent_hash) { Class.new(::Hash) { include Hashie::Extensions::IndifferentAccess } } it 'returns a normal hash without indifference' do indifferent = indifferent_hash.new indifferent['cat'] = 'meow' subject = indifferent.to_hash expect(subject['cat']).to eq 'meow' expect(subject[:cat]).to be_nil end it 'maintains the #default_proc when set' do indifferent = indifferent_hash.new { |_hash, key| "Nothing here: #{key}" } subject = indifferent.to_hash expect(subject['babble']).to eq 'Nothing here: babble' end it 'maintains the #default when set' do indifferent = indifferent_hash.new(0) subject = indifferent.to_hash expect(subject['babble']).to eq 0 end end describe 'when included in dash' do let(:params) { { foo: 'bar' } } subject { IndifferentHashWithDash.new(params) } it 'initialize with a symbol' do expect(subject.foo).to eq params[:foo] end end describe 'when overriding indifferent methods' do let(:indifferent_hash) do Class.new(::Hash) do include Hashie::Extensions::IndifferentAccess ALIASES = { cat: :grumpy }.freeze # Override writer to maintain alias of the given key def indifferent_writer(key, value) indifferent_value = indifferent_value(value) regular_writer convert_key(key), indifferent_value regular_writer convert_key(ALIASES[key]), indifferent_value end alias_method :[]=, :indifferent_writer end.new end it '#indifferent_writer' do indifferent_hash[:cat] = 'meow' expect(indifferent_hash[:cat]).to eq('meow') expect(indifferent_hash['cat']).to eq('meow') expect(indifferent_hash[:grumpy]).to eq('meow') expect(indifferent_hash['grumpy']).to eq('meow') end it '#merge' do merged_hash = indifferent_hash.merge(cat: 'meow') expect(merged_hash[:cat]).to eq('meow') expect(merged_hash['cat']).to eq('meow') expect(merged_hash[:grumpy]).to eq('meow') expect(merged_hash['grumpy']).to eq('meow') end end describe 'when translating properties and ignoring undeclared' do let(:value) { 'baz' } subject { IndifferentHashWithIgnoreUndeclaredAndPropertyTranslation.new(params) } context 'and the hash keys are strings' do let(:params) { { 'bar' => value } } it 'sets the property' do expect(subject[:foo]).to eq value end end context 'and the hash keys are symbols' do let(:params) { { bar: 'baz' } } it 'sets the property' do expect(subject[:foo]).to eq value end end context 'and there are undeclared keys' do let(:params) { { 'bar' => 'baz', 'fail' => false } } it 'sets the property' do expect(subject[:foo]).to eq value end end end shared_examples_for 'hash with indifferent access' do it 'is able to access via string or symbol' do h = subject.build(abc: 123) expect(h[:abc]).to eq 123 expect(h['abc']).to eq 123 end describe '#values_at' do it 'indifferently finds values' do h = subject.build(:foo => 'bar', 'baz' => 'qux') expect(h.values_at('foo', :baz)).to eq %w[bar qux] end it 'returns the same instance of the hash that was set' do hash = {} h = subject.build(foo: hash) expect(h.values_at(:foo)[0]).to be(hash) end it 'returns the same instance of the array that was set' do array = [] h = subject.build(foo: array) expect(h.values_at(:foo)[0]).to be(array) end it 'returns the same instance of the string that was set' do str = 'my string' h = subject.build(foo: str) expect(h.values_at(:foo)[0]).to be(str) end it 'returns the same instance of the object that was set' do object = Object.new h = subject.build(foo: object) expect(h.values_at(:foo)[0]).to be(object) end end describe '#fetch' do it 'works like normal fetch, but indifferent' do h = subject.build(foo: 'bar') expect(h.fetch(:foo)).to eq h.fetch('foo') expect(h.fetch(:foo)).to eq 'bar' end it 'returns the same instance of the hash that was set' do hash = {} h = subject.build(foo: hash) expect(h.fetch(:foo)).to be(hash) end it 'returns the same instance of the array that was set' do array = [] h = subject.build(foo: array) expect(h.fetch(:foo)).to be(array) end it 'returns the same instance of the string that was set' do str = 'my string' h = subject.build(foo: str) expect(h.fetch(:foo)).to be(str) end it 'returns the same instance of the object that was set' do object = Object.new h = subject.build(foo: object) expect(h.fetch(:foo)).to be(object) end it 'yields with key name if key does not exists' do h = subject.build(a: 0) expect(h.fetch(:foo) { |key| ['default for', key] }).to eq ['default for', 'foo'] end end describe '#delete' do it 'deletes indifferently' do h = subject.build(:foo => 'bar', 'baz' => 'qux') h.delete('foo') h.delete(:baz) expect(h).to be_empty end end describe '#key?' do let(:h) { subject.build(foo: 'bar') } it 'finds it indifferently' do expect(h).to be_key(:foo) expect(h).to be_key('foo') end %w[include? member? has_key?].each do |key_alias| it "is aliased as #{key_alias}" do expect(h.send(key_alias.to_sym, :foo)).to be(true) expect(h.send(key_alias.to_sym, 'foo')).to be(true) end end end describe '#update' do let(:h) { subject.build(foo: 'bar') } it 'allows keys to be indifferent still' do h.update(baz: 'qux') expect(h['foo']).to eq 'bar' expect(h['baz']).to eq 'qux' end it 'recursively injects indifference into sub-hashes' do h.update(baz: { qux: 'abc' }) expect(h['baz']['qux']).to eq 'abc' end it 'does not change the ancestors of the injected object class' do h.update(baz: { qux: 'abc' }) expect({}).not_to be_respond_to(:indifferent_access?) end end describe '#replace' do let(:h) { subject.build(foo: 'bar').replace(bar: 'baz', hi: 'bye') } it 'returns self' do expect(h).to be_a(subject) end it 'removes old keys' do [:foo, 'foo'].each do |k| expect(h[k]).to be_nil expect(h.key?(k)).to be_falsy end end it 'creates new keys with indifferent access' do [:bar, 'bar', :hi, 'hi'].each { |k| expect(h.key?(k)).to be_truthy } expect(h[:bar]).to eq 'baz' expect(h['bar']).to eq 'baz' expect(h[:hi]).to eq 'bye' expect(h['hi']).to eq 'bye' end end describe '#try_convert' do describe 'with conversion' do let(:h) { subject.try_convert(foo: 'bar') } it 'is a subject' do expect(h).to be_a(subject) end end describe 'without conversion' do let(:h) { subject.try_convert('{ :foo => bar }') } it 'is nil' do expect(h).to be_nil end end end with_minimum_ruby('2.5.0') do describe '#slice' do let(:h) { subject.build(foo: 'bar', baz: 'qux') } it 'indifferently slices the hash' do sliced_h = { 'foo' => 'bar' } expect(h.slice('foo')).to eq sliced_h expect(h.slice(:foo)).to eq sliced_h end end end with_minimum_ruby('3.0.0') do describe '#except' do let(:h) { subject.build(foo: 'bar', baz: 'qux') } it 'indifferently excepts keys from the hash' do sliced_h = { 'baz' => 'qux' } expect(h.except('foo')).to eq sliced_h expect(h.except(:foo)).to eq sliced_h end end end end describe 'with merge initializer' do subject { IndifferentHashWithMergeInitializer } it_should_behave_like 'hash with indifferent access' end describe 'with array initializer' do subject { IndifferentHashWithArrayInitializer } it_should_behave_like 'hash with indifferent access' end describe 'with try convert initializer' do subject { IndifferentHashWithTryConvertInitializer } it_should_behave_like 'hash with indifferent access' end end hashie-5.0.0/spec/hashie/extensions/indifferent_access_with_rails_hwia_spec.rb000066400000000000000000000000001414227716500277600ustar00rootroot00000000000000hashie-5.0.0/spec/hashie/extensions/key_conversion_spec.rb000066400000000000000000000005071414227716500237560ustar00rootroot00000000000000require 'spec_helper' require 'support/module_context' describe Hashie::Extensions::KeyConversion do include_context 'included hash module' it { should respond_to(:stringify_keys) } it { should respond_to(:stringify_keys!) } it { should respond_to(:symbolize_keys) } it { should respond_to(:symbolize_keys!) } end hashie-5.0.0/spec/hashie/extensions/mash/000077500000000000000000000000001414227716500203105ustar00rootroot00000000000000hashie-5.0.0/spec/hashie/extensions/mash/define_accessors_spec.rb000066400000000000000000000052001414227716500251430ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::Mash::DefineAccessors do let(:args) { [] } shared_examples 'class with dynamically defined accessors' do it 'defines reader on demand' do expect(subject.method_defined?(:foo)).to be_falsey instance.foo expect(subject.method_defined?(:foo)).to be_truthy end it 'defines writer on demand' do expect(subject.method_defined?(:foo=)).to be_falsey instance.foo = :bar expect(subject.method_defined?(:foo=)).to be_truthy end it 'defines predicate on demand' do expect(subject.method_defined?(:foo?)).to be_falsey instance.foo? expect(subject.method_defined?(:foo?)).to be_truthy end it 'defines initializing reader on demand' do expect(subject.method_defined?(:foo!)).to be_falsey instance.foo! expect(subject.method_defined?(:foo!)).to be_truthy end it 'defines underbang reader on demand' do expect(subject.method_defined?(:foo_)).to be_falsey instance.foo_ expect(subject.method_defined?(:foo_)).to be_truthy end context 'when initializing from another hash' do let(:args) { [{ foo: :bar }] } it 'does not define any accessors' do expect(subject.method_defined?(:foo)).to be_falsey expect(subject.method_defined?(:foo=)).to be_falsey expect(subject.method_defined?(:foo?)).to be_falsey expect(subject.method_defined?(:foo!)).to be_falsey expect(subject.method_defined?(:foo_)).to be_falsey expect(instance.foo).to eq :bar end end end context 'when included in Mash subclass' do subject { Class.new(Hashie::Mash) { include Hashie::Extensions::Mash::DefineAccessors } } let(:instance) { subject.new(*args) } describe 'this subclass' do it_behaves_like 'class with dynamically defined accessors' describe 'when accessors are overrided in class' do before do subject.class_eval do def foo if self[:foo] != 1 :bar else super end end end end it 'allows to call super' do expect(instance.foo).to eq :bar instance.foo = 2 expect(instance.foo).to eq :bar instance.foo = 1 expect(instance.foo).to eq 1 end end end end context 'when Mash instance is extended' do let(:instance) { Hashie::Mash.new(*args).with_accessors! } subject { instance.singleton_class } describe 'its singleton class' do it_behaves_like 'class with dynamically defined accessors' end end end hashie-5.0.0/spec/hashie/extensions/mash/keep_original_keys_spec.rb000066400000000000000000000025201414227716500255110ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Hashie::Extensions::Mash::KeepOriginalKeys do let(:keeping_mash) do Class.new(Hashie::Mash) do include Hashie::Extensions::Mash::KeepOriginalKeys end end it 'keeps the keys in the resulting hash identical to the original' do original = { :a => 'apple', 'b' => 'bottle' } mash = keeping_mash.new(original) expect(mash.to_hash).to eq(original) end it 'indifferently responds to keys' do original = { :a => 'apple', 'b' => 'bottle' } mash = keeping_mash.new(original) expect(mash['a']).to eq(mash[:a]) expect(mash['b']).to eq(mash[:b]) end it 'responds to all method accessors like a Mash' do original = { :a => 'apple', 'b' => 'bottle' } mash = keeping_mash.new(original) expect(mash.a).to eq('apple') expect(mash.a?).to eq(true) expect(mash.b).to eq('bottle') expect(mash.b?).to eq(true) expect(mash.underbang_).to be_a(keeping_mash) expect(mash.bang!).to be_a(keeping_mash) expect(mash.predicate?).to eq(false) end it 'keeps the keys that are directly passed without converting them' do original = { :a => 'apple', 'b' => 'bottle' } mash = keeping_mash.new(original) mash[:c] = 'cat' mash['d'] = 'dog' expect(mash.to_hash).to eq(:a => 'apple', 'b' => 'bottle', :c => 'cat', 'd' => 'dog') end end hashie-5.0.0/spec/hashie/extensions/mash/permissive_respond_to_spec.rb000066400000000000000000000021241414227716500262700ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Hashie::Extensions::Mash::PermissiveRespondTo do class PermissiveMash < Hashie::Mash include Hashie::Extensions::Mash::PermissiveRespondTo end it 'allows you to bind to unset getters' do mash = PermissiveMash.new(a: 1) other_mash = PermissiveMash.new(b: 2) expect { mash.method(:b) }.not_to raise_error expect(mash.method(:b).unbind.bind(other_mash).call).to eq 2 end it 'works properly with SimpleDelegator' do delegator = Class.new(SimpleDelegator) do def initialize(hash) super(PermissiveMash.new(hash)) end end foo = delegator.new(a: 1) expect(foo.a).to eq 1 expect { foo.b }.not_to raise_error end context 'warnings' do include_context 'with a logger' it 'does not log a collision when setting normal keys' do PermissiveMash.new(a: 1) expect(logger_output).to be_empty end it 'logs a collision with a built-in method' do PermissiveMash.new(zip: 1) expect(logger_output).to match('PermissiveMash#zip defined in Enumerable') end end end hashie-5.0.0/spec/hashie/extensions/mash/safe_assignment_spec.rb000066400000000000000000000024451414227716500250220ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::Mash::SafeAssignment do class MashWithSafeAssignment < Hashie::Mash include Hashie::Extensions::Mash::SafeAssignment private def my_own_private :hello! end end context 'when included in Mash' do subject { MashWithSafeAssignment.new } context 'when not attempting to override a method' do it 'assigns just fine' do expect do subject.blabla = 'Test' subject.blabla = 'Test' end.to_not raise_error end end context 'when attempting to override a method' do it 'raises an error' do expect { subject.zip = 'Test' }.to raise_error(ArgumentError) end end context 'when attempting to override a private method' do it 'raises an error' do expect { subject.my_own_private = 'Test' }.to raise_error(ArgumentError) end end context 'when attempting to initialize with predefined method' do it 'raises an error' do expect { MashWithSafeAssignment.new(zip: true) }.to raise_error(ArgumentError) end end context 'when setting as a hash key' do it 'still raises if conflicts with a method' do expect { subject[:zip] = 'Test' }.to raise_error(ArgumentError) end end end end hashie-5.0.0/spec/hashie/extensions/mash/symbolize_keys_spec.rb000066400000000000000000000031331414227716500247170ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Hashie::Extensions::Mash::SymbolizeKeys do it 'raises an error when included in a class that is not a Mash' do expect do Class.new do include Hashie::Extensions::Mash::SymbolizeKeys end end.to raise_error(ArgumentError) end context 'when included in a Mash' do class SymbolizedMash < Hashie::Mash include Hashie::Extensions::Mash::SymbolizeKeys end it 'symbolizes string keys in the Mash' do my_mash = SymbolizedMash.new('test' => 'value') expect(my_mash.to_h).to eq(test: 'value') end it 'preserves keys which cannot be symbolized' do my_mash = SymbolizedMash.new( '1' => 'symbolizable one', 1 => 'one', [1, 2, 3] => 'testing', { 'test' => 'value' } => 'value' ) expect(my_mash.to_h).to eq( :'1' => 'symbolizable one', 1 => 'one', [1, 2, 3] => 'testing', { 'test' => 'value' } => 'value' ) end end context 'implicit to_hash on double splat' do let(:destructure) { ->(**opts) { opts } } let(:my_mash) do Class.new(Hashie::Mash) do include Hashie::Extensions::Mash::SymbolizeKeys end end let(:instance) { my_mash.new('outer' => { 'inner' => 42 }, 'testing' => [1, 2, 3]) } subject { destructure.call(**instance) } it 'is converted on method calls' do expect(subject).to eq(outer: { inner: 42 }, testing: [1, 2, 3]) end it 'is converted on explicit operator call' do expect(**instance).to eq(outer: { inner: 42 }, testing: [1, 2, 3]) end end end hashie-5.0.0/spec/hashie/extensions/merge_initializer_spec.rb000066400000000000000000000010351414227716500244200ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::MergeInitializer do class MergeInitializerHash < Hash include Hashie::Extensions::MergeInitializer end subject { MergeInitializerHash } it 'initializes with no arguments' do expect(subject.new).to eq({}) end it 'initializes with a hash' do expect(subject.new(abc: 'def')).to eq(abc: 'def') end it 'initializes with a hash and a default' do h = subject.new({ abc: 'def' }, 'bar') expect(h[:foo]).to eq 'bar' expect(h[:abc]).to eq 'def' end end hashie-5.0.0/spec/hashie/extensions/method_access_spec.rb000066400000000000000000000142461414227716500235270ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::MethodReader do class ReaderHash < Hash include Hashie::Extensions::MethodReader def initialize(hash = {}) update(hash) end end subject { ReaderHash } it 'reads string keys from the method' do expect(subject.new('awesome' => 'sauce').awesome).to eq 'sauce' end it 'reads symbol keys from the method' do expect(subject.new(awesome: 'sauce').awesome).to eq 'sauce' end it 'reads nil and false values out properly' do h = subject.new(nil: nil, false: false) # rubocop:disable Lint/BooleanSymbol expect(h.nil).to eq nil expect(h.false).to eq false end it 'raises a NoMethodError for undefined keys' do expect { subject.new.awesome }.to raise_error(NoMethodError) end it 'returns false for undefined keys if key with question has been called ' do expect(subject.new.awesome?).to eq false end it 'returns true for defined keys if key with question has been called' do expect(subject.new(awesome: 'sauce').awesome?).to eq true end describe '#respond_to?' do it 'is true for string keys' do expect(subject.new('awesome' => 'sauce')).to be_respond_to(:awesome) end it 'is true for symbol keys' do expect(subject.new(awesome: 'sauce')).to be_respond_to(:awesome) end it 'is false for non-keys' do expect(subject.new).not_to be_respond_to(:awesome) end end end describe Hashie::Extensions::MethodWriter do class WriterHash < Hash include Hashie::Extensions::MethodWriter end subject { WriterHash.new } it 'writes from a method call' do subject.awesome = 'sauce' expect(subject['awesome']).to eq 'sauce' end it 'converts the key using the #convert_key method' do allow(subject).to receive(:convert_key).and_return(:awesome) subject.awesome = 'sauce' expect(subject[:awesome]).to eq 'sauce' end it 'raises NoMethodError on non equals-ending methods' do expect { subject.awesome }.to raise_error(NoMethodError) end it '#respond_to? correctly' do expect(subject).to be_respond_to(:abc=) expect(subject).not_to be_respond_to(:abc) end end describe Hashie::Extensions::MethodQuery do class QueryHash < Hash include Hashie::Extensions::MethodQuery def initialize(hash = {}) update(hash) end end subject { QueryHash } it 'is true for non-nil string key values' do expect(subject.new('abc' => 123).abc?).to eq true end it 'is true for non-nil symbol key values' do expect(subject.new(abc: 123).abc?).to eq true end it 'is false for false key values' do expect(subject.new(abc: false).abc?).to eq false end it 'is false for nil key values' do expect(subject.new(abc: nil).abc?).to eq false end it 'raises a NoMethodError for non-set keys' do expect { subject.new.abc? }.to raise_error(NoMethodError) end it '#respond_to? for existing string keys' do expect(subject.new('abc' => 'def')).to be_respond_to('abc?') end it '#respond_to? for existing symbol keys' do expect(subject.new(abc: 'def')).to be_respond_to(:abc?) end it 'does not #respond_to? for non-existent keys' do expect(subject.new).not_to be_respond_to('abc?') end end describe Hashie::Extensions::MethodAccess do it 'includes all of the other method mixins' do klass = Class.new(Hash) klass.send :include, Hashie::Extensions::MethodAccess included_modules = klass.ancestors & [ Hashie::Extensions::MethodReader, Hashie::Extensions::MethodWriter, Hashie::Extensions::MethodQuery ] expect(included_modules.size).to eq 3 end end describe Hashie::Extensions::MethodOverridingWriter do class OverridingHash < Hash include Hashie::Extensions::MethodOverridingWriter end subject { OverridingHash.new } it 'writes from a method call' do subject.awesome = 'sauce' expect(subject['awesome']).to eq 'sauce' end it 'convertes the key using the #convert_key method' do allow(subject).to receive(:convert_key).and_return(:awesome) subject.awesome = 'sauce' expect(subject[:awesome]).to eq 'sauce' end it 'raises NoMethodError on non equals-ending methods' do expect { subject.awesome }.to raise_error(NoMethodError) end it '#respond_to_missing? correctly' do expect(subject).to respond_to(:abc=) expect(subject).not_to respond_to(:abc) expect(subject.method(:abc=)).not_to be_nil end context 'when writing a Hash method' do before { subject.zip = 'a-dee-doo-dah' } it 'overrides the original method' do expect(subject.zip).to eq 'a-dee-doo-dah' end it 'aliases the method with two leading underscores' do expect(subject.__zip).to eq [[%w[zip a-dee-doo-dah]]] end it 'does not re-alias when overriding an already overridden method' do subject.zip = 'test' expect(subject.zip).to eq 'test' expect(subject.__zip).to eq [[%w[zip test]]] end end end describe Hashie::Extensions::MethodAccessWithOverride do it 'includes all of the other method mixins' do mod_list = [ Hashie::Extensions::MethodReader, Hashie::Extensions::MethodOverridingWriter, Hashie::Extensions::MethodQuery, Hashie::Extensions::MethodOverridingInitializer ] klass = Class.new(Hash) klass.send :include, Hashie::Extensions::MethodAccessWithOverride expect((klass.ancestors & mod_list).size).to eq 4 end end describe Hashie::Extensions::MethodOverridingInitializer do class OverridingHash < Hash include Hashie::Extensions::MethodOverridingInitializer end context 'when the key is a string' do subject { OverridingHash.new('zip' => 'a-dee-doo-dah') } it 'overrides the original method' do expect(subject.zip).to eq 'a-dee-doo-dah' end it 'aliases the method with two leading underscores' do expect(subject.__zip).to eq [[%w[zip a-dee-doo-dah]]] end end context 'when the key is a symbol' do subject { OverridingHash.new(zip: 'a-dee-doo-dah') } it 'overrides the original method' do expect(subject.zip).to eq 'a-dee-doo-dah' end it 'aliases the method with two leading underscores' do expect(subject.__zip).to eq [[%w[zip a-dee-doo-dah]]] end end end hashie-5.0.0/spec/hashie/extensions/strict_key_access_spec.rb000066400000000000000000000072361414227716500244300ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::StrictKeyAccess do class StrictKeyAccessHash < Hash include Hashie::Extensions::StrictKeyAccess end shared_examples_for 'StrictKeyAccess with valid key' do |options = {}| before { pending_for(options[:pending]) } if options[:pending] context 'set' do let(:new_value) { 42 } it('returns value') do expect(instance.send(:[]=, valid_key, new_value)).to eq new_value end end context 'access' do it('returns value') do expect(instance[valid_key]).to eq valid_value end end context 'lookup' do it('returns key') do expect(instance.key(valid_value)).to eq valid_key end end end shared_examples_for 'StrictKeyAccess with invalid key' do |options = {}| before { pending_for(options[:pending]) } if options[:pending] context 'access' do it('raises an error') do # Formatting of the error message varies on Rubinius and ruby-head expect { instance[invalid_key] }.to raise_error KeyError end end context 'lookup' do it('raises an error') do # Formatting of the error message does not vary here because raised by StrictKeyAccess expect { instance.key(invalid_value) }.to raise_error KeyError end end end shared_examples_for 'StrictKeyAccess raises KeyError instead of allowing defaults' do context '#default' do it 'raises an error' do expect { instance.default(invalid_key) } .to raise_error Hashie::Extensions::StrictKeyAccess::DefaultError end end context '#default=' do it 'raises an error' do expect { instance.default = invalid_key } .to raise_error Hashie::Extensions::StrictKeyAccess::DefaultError end end context '#default_proc' do it 'raises an error' do expect { instance.default_proc } .to raise_error Hashie::Extensions::StrictKeyAccess::DefaultError end end context '#default_proc=' do it 'raises an error' do expect { instance.default_proc = proc {} } .to raise_error Hashie::Extensions::StrictKeyAccess::DefaultError end end end let(:klass) { StrictKeyAccessHash } let(:instance) { StrictKeyAccessHash.new(*initialization_args) } let(:initialization_args) do [ { valid_key => valid_value } ] end let(:valid_key) { :abc } let(:valid_value) { 'def' } let(:invalid_key) { :mega } let(:invalid_value) { 'death' } context '.new' do context 'no defaults at initialization' do let(:initialization_args) { [] } before do instance.merge!(valid_key => valid_value) end it_behaves_like 'StrictKeyAccess with valid key' it_behaves_like 'StrictKeyAccess with invalid key' it_behaves_like 'StrictKeyAccess raises KeyError instead of allowing defaults' end context 'with defaults at initialization' do before do instance.merge!(valid_key => valid_value) end it_behaves_like 'StrictKeyAccess with valid key' it_behaves_like 'StrictKeyAccess with invalid key' it_behaves_like 'StrictKeyAccess raises KeyError instead of allowing defaults' end it_behaves_like 'StrictKeyAccess with invalid key' it_behaves_like 'StrictKeyAccess raises KeyError instead of allowing defaults' end context '.[]' do let(:instance) { StrictKeyAccessHash[*initialization_args] } it_behaves_like 'StrictKeyAccess with valid key', pending: { engine: 'rbx' } it_behaves_like 'StrictKeyAccess with invalid key', pending: { engine: 'rbx' } it_behaves_like 'StrictKeyAccess raises KeyError instead of allowing defaults' end end hashie-5.0.0/spec/hashie/extensions/stringify_keys_spec.rb000066400000000000000000000061331414227716500237730ustar00rootroot00000000000000require 'spec_helper' require 'support/module_context' def invoke(method) if subject == object subject.public_send(method) else subject.public_send(method, object) end end shared_examples 'stringify_keys!' do it 'converts keys to strings' do object[:abc] = 'abc' object[123] = '123' invoke :stringify_keys! expect((object.keys & %w[abc 123]).size).to eq 2 end it 'converts nested instances of the same class' do object[:ab] = dummy_class.new object[:ab][:cd] = dummy_class.new object[:ab][:cd][:ef] = 'abcdef' invoke :stringify_keys! expect(object).to eq('ab' => { 'cd' => { 'ef' => 'abcdef' } }) end it 'converts nested hashes' do object[:ab] = { cd: { ef: 'abcdef' } } invoke :stringify_keys! expect(object).to eq('ab' => { 'cd' => { 'ef' => 'abcdef' } }) end it 'converts nested arrays' do object[:ab] = [] object[:ab] << dummy_class.new object[:ab] << dummy_class.new object[:ab][0][:cd] = 'abcd' object[:ab][1][:ef] = 'abef' invoke :stringify_keys! expect(object).to eq('ab' => [{ 'cd' => 'abcd' }, { 'ef' => 'abef' }]) end end shared_examples 'stringify_keys' do it 'converts keys to strings' do object[:abc] = 'def' copy = invoke :stringify_keys expect(copy['abc']).to eq 'def' end it 'does not alter the original' do object[:abc] = 'def' copy = invoke :stringify_keys expect(object.keys).to eq [:abc] expect(copy.keys).to eq %w[abc] end end describe Hashie::Extensions::StringifyKeys do include_context 'included hash module' let(:object) { subject } describe '#stringify_keys!' do include_examples 'stringify_keys!' it 'returns itself' do expect(subject.stringify_keys!).to eq subject end end context 'class methods' do subject { described_class } let(:object) { {} } describe '.stringify_keys' do include_examples 'stringify_keys' end describe '.stringify_keys!' do include_examples 'stringify_keys!' end end context 'singleton methods' do subject { Hash } let(:object) { subject.new.merge(a: 1, b: { c: 2 }).extend(Hashie::Extensions::StringifyKeys) } let(:expected_hash) { { 'a' => 1, 'b' => { 'c' => 2 } } } describe '.stringify_keys' do it 'does not raise error' do expect { object.stringify_keys } .not_to raise_error end it 'produces expected stringified hash' do expect(object.stringify_keys).to eq(expected_hash) end end describe '.stringify_keys!' do it 'does not raise error' do expect { object.stringify_keys! } .not_to raise_error end it 'produces expected stringified hash' do expect(object.stringify_keys!).to eq(expected_hash) end end end end describe Hashie do let!(:dummy_class) do klass = Class.new(::Hash) klass.send :include, Hashie::Extensions::StringifyKeys klass end subject { described_class } let(:object) { {} } describe '.stringify_keys' do include_examples 'stringify_keys' end describe '.stringify_keys!' do include_examples 'stringify_keys!' end end hashie-5.0.0/spec/hashie/extensions/symbolize_keys_spec.rb000066400000000000000000000064411414227716500237740ustar00rootroot00000000000000require 'spec_helper' require 'support/module_context' def invoke(method) if subject == object subject.public_send(method) else subject.public_send(method, object) end end shared_examples 'symbolize_keys!' do it 'converts keys to symbols' do object['abc'] = 'abc' object['def'] = 'def' invoke :symbolize_keys! expect((object.keys & %i[abc def]).size).to eq 2 end it 'converts nested instances of the same class' do object['ab'] = dummy_class.new object['ab']['cd'] = dummy_class.new object['ab']['cd']['ef'] = 'abcdef' invoke :symbolize_keys! expect(object).to eq(ab: { cd: { ef: 'abcdef' } }) end it 'converts nested hashes' do object['ab'] = { 'cd' => { 'ef' => 'abcdef' } } invoke :symbolize_keys! expect(object).to eq(ab: { cd: { ef: 'abcdef' } }) end it 'performs deep conversion within nested arrays' do object['ab'] = [] object['ab'] << dummy_class.new object['ab'] << dummy_class.new object['ab'][0]['cd'] = 'abcd' object['ab'][1]['ef'] = 'abef' new_object = invoke :symbolize_keys expect(new_object).to eq(ab: [{ cd: 'abcd' }, { ef: 'abef' }]) end end shared_examples 'symbolize_keys' do it 'converts keys to symbols' do object['abc'] = 'def' copy = invoke :symbolize_keys expect(copy[:abc]).to eq 'def' end it 'does not alter the original' do object['abc'] = 'def' copy = invoke :symbolize_keys expect(object.keys).to eq ['abc'] expect(copy.keys).to eq [:abc] end end describe Hashie::Extensions::SymbolizeKeys do include_context 'included hash module' let(:object) { subject } describe '#symbolize_keys!' do include_examples 'symbolize_keys!' let(:object) { subject } it 'returns itself' do expect(subject.symbolize_keys!).to eq subject end end describe '#symbolize_keys' do include_examples 'symbolize_keys' end context 'class methods' do subject { described_class } let(:object) { {} } describe '.symbolize_keys' do include_examples 'symbolize_keys' end describe '.symbolize_keys!' do include_examples 'symbolize_keys!' end end context 'singleton methods' do subject { Hash } let(:object) do subject.new.merge('a' => 1, 'b' => { 'c' => 2 }, 1 => 'numeric key') .extend(Hashie::Extensions::SymbolizeKeys) end let(:expected_hash) { { a: 1, b: { c: 2 }, 1 => 'numeric key' } } describe '.symbolize_keys' do it 'does not raise error' do expect { object.symbolize_keys }.not_to raise_error end it 'produces expected symbolized hash' do expect(object.symbolize_keys).to eq(expected_hash) end end describe '.symbolize_keys!' do it 'does not raise error' do expect { object.symbolize_keys! }.not_to raise_error end it 'produces expected symbolized hash' do expect(object.symbolize_keys!).to eq(expected_hash) end end end end describe Hashie do let!(:dummy_class) do klass = Class.new(::Hash) klass.send :include, Hashie::Extensions::StringifyKeys klass end subject { described_class } let(:object) { {} } describe '.symbolize_keys' do include_examples 'symbolize_keys' end describe '.symbolize_keys!' do include_examples 'symbolize_keys!' end end hashie-5.0.0/spec/hashie/hash_spec.rb000066400000000000000000000074731414227716500174560ustar00rootroot00000000000000require 'spec_helper' describe Hash do it 'is convertible to a Hashie::Mash' do mash = Hashie::Hash[some: 'hash'].to_mash expect(mash.is_a?(Hashie::Mash)).to be_truthy expect(mash.some).to eq 'hash' end it '#stringify_keys! turns all keys into strings' do hash = Hashie::Hash[a: 'hey', 123 => 'bob'] hash.stringify_keys! expect(hash).to eq Hashie::Hash['a' => 'hey', '123' => 'bob'] end it '#stringify_keys! turns all keys into strings recursively' do hash = Hashie::Hash[a: 'hey', 123 => { 345 => 'hey' }] hash.stringify_keys! expect(hash).to eq Hashie::Hash['a' => 'hey', '123' => { '345' => 'hey' }] end it '#stringify_keys returns a hash with stringified keys' do hash = Hashie::Hash[a: 'hey', 123 => 'bob'] stringified_hash = hash.stringify_keys expect(hash).to eq Hashie::Hash[a: 'hey', 123 => 'bob'] expect(stringified_hash).to eq Hashie::Hash['a' => 'hey', '123' => 'bob'] end it '#to_hash returns a hash with same keys' do hash = Hashie::Hash['a' => 'hey', 123 => 'bob', 'array' => [1, 2, 3]] stringified_hash = hash.to_hash expect(stringified_hash).to eq('a' => 'hey', 123 => 'bob', 'array' => [1, 2, 3]) end it '#to_hash with stringify_keys set to true returns a hash with stringified_keys' do hash = Hashie::Hash['a' => 'hey', 123 => 'bob', 'array' => [1, 2, 3]] symbolized_hash = hash.to_hash(stringify_keys: true) expect(symbolized_hash).to eq('a' => 'hey', '123' => 'bob', 'array' => [1, 2, 3]) end it '#to_hash with symbolize_keys set to true returns a hash with symbolized keys' do hash = Hashie::Hash['a' => 'hey', 123 => 'bob', 'array' => [1, 2, 3]] symbolized_hash = hash.to_hash(symbolize_keys: true) expect(symbolized_hash).to eq(a: 'hey', 123 => 'bob', array: [1, 2, 3]) end it "#to_hash should not blow up when #to_hash doesn't accept arguments" do class BareCustomMash < Hashie::Mash def to_hash {} end end h = Hashie::Hash.new h[:key] = BareCustomMash.new expect { h.to_hash }.not_to raise_error end describe 'when the value is an object that respond_to to_hash' do class ClassRespondsToHash def to_hash(options = {}) Hashie::Hash['a' => 'hey', b: 'bar', 123 => 'bob', 'array' => [1, 2, 3]].to_hash(options) end end it '#to_hash returns a hash with same keys' do hash = Hashie::Hash[ 'a' => 'hey', 123 => 'bob', 'array' => [1, 2, 3], subhash: ClassRespondsToHash.new ] stringified_hash = hash.to_hash expected = { 'a' => 'hey', 123 => 'bob', 'array' => [1, 2, 3], subhash: { 'a' => 'hey', b: 'bar', 123 => 'bob', 'array' => [1, 2, 3] } } expect(stringified_hash).to eq(expected) end it '#to_hash with stringify_keys set to true returns a hash with stringified_keys' do hash = Hashie::Hash[ 'a' => 'hey', 123 => 'bob', 'array' => [1, 2, 3], subhash: ClassRespondsToHash.new ] symbolized_hash = hash.to_hash(stringify_keys: true) expected = { 'a' => 'hey', '123' => 'bob', 'array' => [1, 2, 3], 'subhash' => { 'a' => 'hey', 'b' => 'bar', '123' => 'bob', 'array' => [1, 2, 3] } } expect(symbolized_hash).to eq(expected) end it '#to_hash with symbolize_keys set to true returns a hash with symbolized keys' do hash = Hashie::Hash[ 'a' => 'hey', 123 => 'bob', 'array' => [1, 2, 3], subhash: ClassRespondsToHash.new ] symbolized_hash = hash.to_hash(symbolize_keys: true) expected = { a: 'hey', 123 => 'bob', array: [1, 2, 3], subhash: { a: 'hey', b: 'bar', 123 => 'bob', array: [1, 2, 3] } } expect(symbolized_hash).to eq(expected) end end end hashie-5.0.0/spec/hashie/mash_spec.rb000066400000000000000000001055751414227716500174650ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Mash do subject { Hashie::Mash.new } include_context 'with a logger' it 'inherits from Hash' do expect(subject.is_a?(Hash)).to be_truthy end it 'sets hash values through method= calls' do subject.test = 'abc' expect(subject['test']).to eq 'abc' end it 'retrieves set values through method calls' do subject['test'] = 'abc' expect(subject.test).to eq 'abc' end it 'retrieves set values through blocks' do subject['test'] = 'abc' value = nil subject.[]('test') { |v| value = v } expect(value).to eq 'abc' end it 'retrieves set values through blocks with method calls' do subject['test'] = 'abc' value = nil subject.test { |v| value = v } expect(value).to eq 'abc' end it 'tests for already set values when passed a ? method' do expect(subject.test?).to be_falsy subject.test = 'abc' expect(subject.test?).to be_truthy end it 'returns false on a ? method if a value has been set to nil or false' do subject.test = nil expect(subject).not_to be_test subject.test = false expect(subject).not_to be_test end it 'makes all [] and []= into strings for consistency' do subject['abc'] = 123 expect(subject.key?('abc')).to be_truthy expect(subject['abc']).to eq 123 end it 'has a to_s that is identical to its inspect' do subject.abc = 123 expect(subject.to_s).to eq subject.inspect end it 'returns nil instead of raising an error for attribute-esque method calls' do expect(subject.abc).to be_nil end it 'returns the default value if set like Hash' do subject.default = 123 expect(subject.abc).to eq 123 end it 'gracefully handles being accessed with arguments' do expect(subject.abc('foobar')).to eq nil subject.abc = 123 expect(subject.abc('foobar')).to eq 123 end # Added due to downstream gems assuming indifferent access to be true for Mash # When this is not, bump major version so that downstream gems can target # correct version and fix accordingly. # See https://github.com/hashie/hashie/pull/197 it 'maintains indifferent access when nested' do subject[:a] = { b: 'c' } expect(subject[:a][:b]).to eq 'c' expect(subject[:a]['b']).to eq 'c' end it 'returns a Hashie::Mash when passed a bang method to a non-existenct key' do expect(subject.abc!.is_a?(Hashie::Mash)).to be_truthy end it 'returns the existing value when passed a bang method for an existing key' do subject.name = 'Bob' expect(subject.name!).to eq 'Bob' end it 'returns a Hashie::Mash when passed an under bang method to a non-existenct key' do expect(subject.abc_.is_a?(Hashie::Mash)).to be_truthy end it 'returns the existing value when passed an under bang method for an existing key' do subject.name = 'Bob' expect(subject.name_).to eq 'Bob' end it '#initializing_reader returns a Hashie::Mash when passed a non-existent key' do expect(subject.initializing_reader(:abc).is_a?(Hashie::Mash)).to be_truthy end it 'allows for multi-level assignment through bang methods' do subject.author!.name = 'Michael Bleigh' expect(subject.author).to eq Hashie::Mash.new(name: 'Michael Bleigh') subject.author!.website!.url = 'http://www.mbleigh.com/' expect(subject.author.website).to eq Hashie::Mash.new(url: 'http://www.mbleigh.com/') end it 'allows for multi-level under bang testing' do expect(subject.author_.website_.url).to be_nil expect(subject.author_.website_.url?).to eq false expect(subject.author).to be_nil end it 'does not call super if id is not a key' do expect(subject.id).to eq nil end it 'returns the value if id is a key' do subject.id = 'Steve' expect(subject.id).to eq 'Steve' end it 'does not call super if type is not a key' do expect(subject.type).to eq nil end it 'returns the value if type is a key' do subject.type = 'Steve' expect(subject.type).to eq 'Steve' end include_context 'with a logger' do it 'logs a warning when overriding built-in methods' do Hashie::Mash.new('trust' => { 'two' => 2 }) expect(logger_output).to match('Hashie::Mash#trust') end it 'can set keys more than once and does not warn when doing so' do mash = Hashie::Mash.new mash[:test_key] = 'Test value' expect { mash[:test_key] = 'A new value' }.not_to raise_error expect(logger_output).to be_empty end it 'does not write to the logger when warnings are disabled' do mash_class = Class.new(Hashie::Mash) do disable_warnings end mash_class.new('trust' => { 'two' => 2 }) expect(logger_output).to be_empty end it 'does not write to the logger when setting most affixed keys' do underbang = Hashie::Mash.new('foo_' => 'foo') bang = Hashie::Mash.new('foo!' => 'foo') query = Hashie::Mash.new('foo?' => 'foo') expect(logger_output).to be_empty expect(underbang.foo_).to eq 'foo' expect(bang.foo!).to eq 'foo' expect(query.foo?).to eq 'foo' end it 'warns when setting a key that looks like a setter' do setter = Hashie::Mash.new('foo=' => 'foo') expect(logger_output).to match 'Hashie::Mash#foo=' expect('setter.foo=').not_to parse_as_valid_ruby setter.foo = 'bar' expect(setter.to_h).to eq 'foo=' => 'foo' end it 'cannot disable logging on the base Mash' do expected_error = Hashie::Extensions::KeyConflictWarning::CannotDisableMashWarnings expect { Hashie::Mash.disable_warnings }.to raise_error(expected_error) end it 'carries over the disable for warnings on grandchild classes' do child_class = Class.new(Hashie::Mash) do disable_warnings end grandchild_class = Class.new(child_class) grandchild_class.new('trust' => { 'two' => 2 }) expect(logger_output).to be_empty end it 'writes to logger when a key is overridden that is not ignored' do mash_class = Class.new(Hashie::Mash) do disable_warnings :merge end mash_class.new('address' => { 'zip' => '90210' }) expect(logger_output).not_to be_empty end it 'does not write to logger when a key is overridden that is ignored' do mash_class = Class.new(Hashie::Mash) do disable_warnings :zip end mash_class.new('address' => { 'zip' => '90210' }) expect(logger_output).to be_empty end it 'carries over the ignored warnings list for warnings on grandchild classes' do child_class = Class.new(Hashie::Mash) do disable_warnings :zip, :merge end grandchild_class = Class.new(child_class) grandchild_class.new('address' => { 'zip' => '90210' }, 'merge' => true) expect(grandchild_class.disabled_warnings).to eq(%i[zip merge]) expect(logger_output).to be_empty end context 'multiple disable_warnings calls' do context 'calling disable_warnings multiple times with parameters' do it 'appends each new parameter to the ignore list' do child_class = Class.new(Hashie::Mash) do disable_warnings :zip disable_warnings :merge disable_warnings :cycle end expect(child_class.disabled_warnings).to eq(%i[zip merge cycle]) end end context 'calling disable_warnings without keys after calling with keys' do it 'uses the last call to determine the ignore list' do child_class = Class.new(Hashie::Mash) do disable_warnings :zip disable_warnings end child_class.new('address' => { 'zip' => '90210' }, 'merge' => true, 'cycle' => 'bi') expect(child_class.disabled_warnings).to eq([]) expect(logger_output).to be_empty end end context 'calling disable_parameters with keys after calling without keys' do it 'only ignores logging for ignored methods' do child_class = Class.new(Hashie::Mash) do disable_warnings disable_warnings :zip end child_class.new('address' => { 'zip' => '90210' }, 'merge' => true) expect(logger_output).to match(/#{child_class}#merge/) expect(logger_output).not_to match(/#{child_class}#zip/) end end end end context 'updating' do subject do described_class.new( first_name: 'Michael', last_name: 'Bleigh', details: { email: 'michael@asf.com', address: 'Nowhere road' } ) end describe '#deep_update' do it 'recursively Hashie::Mash Hashie::Mashes and hashes together' do subject.deep_update(details: { email: 'michael@intridea.com', city: 'Imagineton' }) expect(subject.first_name).to eq 'Michael' expect(subject.details.email).to eq 'michael@intridea.com' expect(subject.details.address).to eq 'Nowhere road' expect(subject.details.city).to eq 'Imagineton' end it 'converts values only once' do class ConvertedMash < Hashie::Mash end rhs = ConvertedMash.new(email: 'foo@bar.com') expect(subject).to receive(:convert_value).exactly(1).times subject.deep_update(rhs) end it 'makes #update deep by default' do expect(subject.update(details: { address: 'Fake street' })).to eql(subject) expect(subject.details.address).to eq 'Fake street' expect(subject.details.email).to eq 'michael@asf.com' end it 'clones before a #deep_merge' do duped = subject.deep_merge(details: { address: 'Fake street' }) expect(duped).not_to eql(subject) expect(duped.details.address).to eq 'Fake street' expect(subject.details.address).to eq 'Nowhere road' expect(duped.details.email).to eq 'michael@asf.com' end it 'default #merge is deep' do duped = subject.merge(details: { email: 'michael@intridea.com' }) expect(duped).not_to eql(subject) expect(duped.details.email).to eq 'michael@intridea.com' expect(duped.details.address).to eq 'Nowhere road' end # http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-update it 'accepts a block' do duped = subject.merge(details: { address: 'Pasadena CA' }) do |_, oldv, newv| [oldv, newv].join(', ') end expect(duped.details.address).to eq 'Nowhere road, Pasadena CA' end it 'copies values for non-duplicate keys when a block is supplied' do m_hash = { details: { address: 'Pasadena CA', state: 'West Thoughtleby' } } duped = subject.merge(m_hash) { |_, oldv, _| oldv } expect(duped.details.address).to eq 'Nowhere road' expect(duped.details.state).to eq 'West Thoughtleby' end it 'does not raise an exception when default_proc raises an error' do hash = described_class.new(a: 1) { |_k, _v| raise('Should not be raise I') } other_has = described_class.new(a: 2, b: 2) { |_k, _v| raise('Should not be raise II') } expected_hash = described_class.new(a: 2, b: 2) res = hash.merge(other_has) expect(res).to eq(expected_hash) end end describe 'shallow update' do it 'shallowly Hashie::Mash Hashie::Mashes and hashes together' do expect(subject.shallow_update(details: { email: 'michael@intridea.com', city: 'Imagineton' })).to eql(subject) expect(subject.first_name).to eq 'Michael' expect(subject.details.email).to eq 'michael@intridea.com' expect(subject.details.address).to be_nil expect(subject.details.city).to eq 'Imagineton' end it 'clones before a #regular_merge' do duped = subject.shallow_merge(details: { address: 'Fake street' }) expect(duped).not_to eql(subject) end it 'default #merge is shallow' do duped = subject.shallow_merge(details: { address: 'Fake street' }) expect(duped.details.address).to eq 'Fake street' expect(subject.details.address).to eq 'Nowhere road' expect(duped.details.email).to be_nil end end describe '#replace' do before do subject.replace( middle_name: 'Cain', details: { city: 'Imagination' } ) end it 'returns self' do expect(subject.replace(foo: 'bar').to_hash).to eq('foo' => 'bar') end it 'sets all specified keys to their corresponding values' do expect(subject.middle_name?).to be_truthy expect(subject.details?).to be_truthy expect(subject.middle_name).to eq 'Cain' expect(subject.details.city?).to be_truthy expect(subject.details.city).to eq 'Imagination' end it 'leaves only specified keys' do expect(subject.keys.sort).to eq %w[details middle_name] expect(subject.first_name?).to be_falsy expect(subject).not_to respond_to(:first_name) expect(subject.last_name?).to be_falsy expect(subject).not_to respond_to(:last_name) end end describe 'delete' do it 'deletes with String key' do subject.delete('details') expect(subject.details).to be_nil expect(subject).not_to be_respond_to :details end it 'deletes with Symbol key' do subject.delete(:details) expect(subject.details).to be_nil expect(subject).not_to be_respond_to :details end end end it 'converts hash assignments into Hashie::Mashes' do subject.details = { email: 'randy@asf.com', address: { state: 'TX' } } expect(subject.details.email).to eq 'randy@asf.com' expect(subject.details.address.state).to eq 'TX' end it 'does not convert the type of Hashie::Mashes childs to Hashie::Mash' do class MyMash < Hashie::Mash end record = MyMash.new record.son = MyMash.new expect(record.son.class).to eq MyMash end it 'does not change the class of Mashes when converted' do class SubMash < Hashie::Mash end record = Hashie::Mash.new son = SubMash.new record['submash'] = son expect(record['submash']).to be_kind_of(SubMash) end it 'respects the class when passed a bang method for a non-existent key' do record = Hashie::Mash.new expect(record.non_existent!).to be_kind_of(Hashie::Mash) class SubMash < Hashie::Mash end son = SubMash.new expect(son.non_existent!).to be_kind_of(SubMash) end it 'respects the class when passed an under bang method for a non-existent key' do record = Hashie::Mash.new expect(record.non_existent_).to be_kind_of(Hashie::Mash) class SubMash < Hashie::Mash end son = SubMash.new expect(son.non_existent_).to be_kind_of(SubMash) end it 'respects the class when converting the value' do record = Hashie::Mash.new record.details = Hashie::Mash.new(email: 'randy@asf.com') expect(record.details).to be_kind_of(Hashie::Mash) end it 'respects another subclass when converting the value' do record = Hashie::Mash.new class SubMash < Hashie::Mash end son = SubMash.new(email: 'foo@bar.com') record.details = son expect(record.details).to be_kind_of(SubMash) end describe '#respond_to?' do subject do Hashie::Mash.new(abc: 'def') end it 'responds to a normal method' do expect(subject).to be_respond_to(:key?) end it 'responds to a set key' do expect(subject).to be_respond_to(:abc) expect(subject.method(:abc)).to_not be_nil end it 'responds to a set key with a suffix' do %w[= ? ! _].each do |suffix| expect(subject).to be_respond_to(:"abc#{suffix}") end end it 'is able to access the suffixed key as a method' do %w[= ? ! _].each do |suffix| expect(subject.method(:"abc#{suffix}")).to_not be_nil end end it 'responds to an unknown key with a suffix' do %w[= ? ! _].each do |suffix| expect(subject).to be_respond_to(:"xyz#{suffix}") end end it 'is able to access an unknown suffixed key as a method' do # See https://github.com/intridea/hashie/pull/285 for more information pending_for(engine: 'ruby', versions: %w[2.2.0 2.2.1 2.2.2]) %w[= ? ! _].each do |suffix| expect(subject.method(:"xyz#{suffix}")).to_not be_nil end end it 'does not respond to an unknown key without a suffix' do expect(subject).not_to be_respond_to(:xyz) expect { subject.method(:xyz) }.to raise_error(NameError) end end context '#initialize' do it 'converts an existing hash to a Hashie::Mash' do converted = Hashie::Mash.new(abc: 123, name: 'Bob') expect(converted.abc).to eq 123 expect(converted.name).to eq 'Bob' end it 'converts hashes recursively into Hashie::Mashes' do converted = Hashie::Mash.new(a: { b: 1, c: { d: 23 } }) expect(converted.a.is_a?(Hashie::Mash)).to be_truthy expect(converted.a.b).to eq 1 expect(converted.a.c.d).to eq 23 end it 'converts hashes in arrays into Hashie::Mashes' do converted = Hashie::Mash.new(a: [{ b: 12 }, 23]) expect(converted.a.first.b).to eq 12 expect(converted.a.last).to eq 23 end it 'converts an existing Hashie::Mash into a Hashie::Mash' do initial = Hashie::Mash.new(name: 'randy', address: { state: 'TX' }) copy = Hashie::Mash.new(initial) expect(initial.name).to eq copy.name expect(initial.__id__).not_to eq copy.__id__ expect(copy.address.state).to eq 'TX' copy.address.state = 'MI' expect(initial.address.state).to eq 'TX' expect(copy.address.__id__).not_to eq initial.address.__id__ end it 'accepts a default block' do initial = Hashie::Mash.new { |h, i| h[i] = [] } expect(initial.default_proc).not_to be_nil expect(initial.default).to be_nil expect(initial.test).to eq [] expect(initial.test?).to be_truthy end it 'allows propagation of a default block' do h = Hashie::Mash.new { |mash, key| mash[key] = mash.class.new(&mash.default_proc) } expect { h[:x][:y][:z] = :xyz }.not_to raise_error expect(h.x.y.z).to eq(:xyz) expect(h[:x][:y][:z]).to eq(:xyz) end it 'allows assignment of an empty array in a default block' do initial = Hashie::Mash.new { |h, k| h[k] = [] } initial.hello << 100 expect(initial.hello).to eq [100] initial['hi'] << 100 expect(initial['hi']).to eq [100] end it 'allows assignment of a non-empty array in a default block' do initial = Hashie::Mash.new { |h, k| h[k] = [100] } initial.hello << 200 expect(initial.hello).to eq [100, 200] initial['hi'] << 200 expect(initial['hi']).to eq [100, 200] end it 'allows assignment of an empty hash in a default block' do initial = Hashie::Mash.new { |h, k| h[k] = {} } initial.hello[:a] = 100 expect(initial.hello).to eq Hashie::Mash.new(a: 100) initial[:hi][:a] = 100 expect(initial[:hi]).to eq Hashie::Mash.new(a: 100) end it 'allows assignment of a non-empty hash in a default block' do initial = Hashie::Mash.new { |h, k| h[k] = { a: 100 } } initial.hello[:b] = 200 expect(initial.hello).to eq Hashie::Mash.new(a: 100, b: 200) initial[:hi][:b] = 200 expect(initial[:hi]).to eq Hashie::Mash.new(a: 100, b: 200) end it 'converts Hashie::Mashes within Arrays back to Hashes' do initial_hash = { 'a' => [{ 'b' => 12, 'c' => ['d' => 50, 'e' => 51] }, 23] } converted = Hashie::Mash.new(initial_hash) expect(converted.to_hash['a'].first.is_a?(Hashie::Mash)).to be_falsy expect(converted.to_hash['a'].first.is_a?(Hash)).to be_truthy expect(converted.to_hash['a'].first['c'].first.is_a?(Hashie::Mash)).to be_falsy end it 'only stringifies keys which can be converted to symbols' do initial_hash = { 1 => 'a', ['b'] => 2, 'c' => 3, d: 4 } converted = Hashie::Mash.new(initial_hash) expect(converted).to eq(1 => 'a', ['b'] => 2, 'c' => 3, 'd' => 4) end it 'preserves keys which cannot be converted to symbols' do initial_hash = { 1 => 'a', '1' => 'b', :'1' => 'c' } converted = Hashie::Mash.new(initial_hash) expect(converted).to eq(1 => 'a', '1' => 'c') end end describe '#fetch' do let(:hash) { { one: 1, other: false } } let(:mash) { Hashie::Mash.new(hash) } context 'when key exists' do it 'returns the value' do expect(mash.fetch(:one)).to eql(1) end it 'returns the value even if the value is falsy' do expect(mash.fetch(:other)).to eql(false) end context 'when key has other than original but acceptable type' do it 'returns the value' do expect(mash.fetch('one')).to eql(1) end end end context 'when key does not exist' do it 'raises KeyError' do error = RUBY_VERSION =~ /1.8/ ? IndexError : KeyError expect { mash.fetch(:two) }.to raise_error(error) end context 'with default value given' do it 'returns default value' do expect(mash.fetch(:two, 8)).to eql(8) end it 'returns default value even if it is falsy' do expect(mash.fetch(:two, false)).to eql(false) end end context 'with block given' do it 'returns default value' do expect(mash.fetch(:two) do 'block default value' end).to eql('block default value') end end end end describe '#to_hash' do let(:hash) { { 'outer' => { 'inner' => 42 }, 'testing' => [1, 2, 3] } } let(:mash) { Hashie::Mash.new(hash) } it 'returns a standard Hash' do expect(mash.to_hash).to be_a(::Hash) end it 'includes all keys' do expect(mash.to_hash.keys).to eql(%w[outer testing]) end it 'converts keys to symbols when symbolize_keys option is true' do expect(mash.to_hash(symbolize_keys: true).keys).to include(:outer) expect(mash.to_hash(symbolize_keys: true).keys).not_to include('outer') end it 'leaves keys as strings when symbolize_keys option is false' do expect(mash.to_hash(symbolize_keys: false).keys).to include('outer') expect(mash.to_hash(symbolize_keys: false).keys).not_to include(:outer) end it 'symbolizes keys recursively' do expect(mash.to_hash(symbolize_keys: true)[:outer].keys).to include(:inner) expect(mash.to_hash(symbolize_keys: true)[:outer].keys).not_to include('inner') end end describe '#stringify_keys' do it 'turns all keys into strings recursively' do hash = Hashie::Mash[:a => 'hey', 123 => { 345 => 'hey' }] hash.stringify_keys! expect(hash).to eq Hashie::Hash['a' => 'hey', '123' => { '345' => 'hey' }] end end describe '#values_at' do let(:hash) { { 'key_one' => 1, :key_two => 2 } } let(:mash) { Hashie::Mash.new(hash) } context 'when the original type is given' do it 'returns the values' do expect(mash.values_at('key_one', :key_two)).to eq([1, 2]) end end context 'when a different, but acceptable type is given' do it 'returns the values' do expect(mash.values_at(:key_one, 'key_two')).to eq([1, 2]) end end context 'when a key is given that is not in the Mash' do it 'returns nil for that value' do expect(mash.values_at('key_one', :key_three)).to eq([1, nil]) end end end describe '.load(filename, options = {})' do let(:config) do { 'production' => { 'foo' => 'production_foo' } } end let(:path) { 'database.yml' } let(:parser) { double(:parser) } subject { described_class.load(path, parser: parser) } before do |ex| unless ex.metadata == :test_cache described_class.instance_variable_set('@_mashes', nil) # clean the cached mashes end end context 'if the file exists' do before do expect(File).to receive(:file?).with(path).and_return(true) expect(parser).to receive(:perform).with(path, {}).and_return(config) end it { is_expected.to be_a(Hashie::Mash) } it 'return a Mash from a file' do expect(subject.production).not_to be_nil expect(subject.production.keys).to eq config['production'].keys expect(subject.production.foo).to eq config['production']['foo'] end it 'freeze the attribtues' do expect { subject.production = {} }.to raise_exception(RuntimeError, /can't modify frozen/) end end context 'if the fils does not exists' do before do expect(File).to receive(:file?).with(path).and_return(false) end it 'raise an ArgumentError' do expect { subject }.to raise_exception(ArgumentError) end end context 'if the file is passed as Pathname' do require 'pathname' let(:path) { Pathname.new('database.yml') } before do expect(File).to receive(:file?).with(path).and_return(true) expect(parser).to receive(:perform).with(path, {}).and_return(config) end it 'return a Mash from a file' do expect(subject.production.foo).to eq config['production']['foo'] end end describe 'results are cached' do let(:parser) { double(:parser) } subject { described_class.load(path, parser: parser) } before do expect(File).to receive(:file?).with(path).and_return(true) expect(File).to receive(:file?).with("#{path}+1").and_return(true) expect(parser).to receive(:perform).once.with(path, {}).and_return(config) expect(parser).to receive(:perform).once.with("#{path}+1", {}).and_return(config) end it 'cache the loaded yml file', :test_cache do 2.times do expect(subject).to be_a(described_class) expect(described_class.load("#{path}+1", parser: parser)).to be_a(described_class) end expect(subject.object_id).to eq subject.object_id end end context 'when the file has aliases in it' do it 'can use the aliases and does not raise an error' do mash = Hashie::Mash.load('spec/fixtures/yaml_with_aliases.yml') expect(mash.company_a.accounts.admin.password).to eq('secret') end it 'can override the value of aliases' do expect do Hashie::Mash.load('spec/fixtures/yaml_with_aliases.yml', aliases: false) end.to raise_error Psych::BadAlias, /base_accounts/ end end context 'when the file has symbols' do it 'can override the value of permitted_classes' do mash = Hashie::Mash.load('spec/fixtures/yaml_with_symbols.yml', permitted_classes: [Symbol]) expect(mash.user_icon.width).to eq(200) end it 'uses defaults for permitted_classes' do expect do Hashie::Mash.load('spec/fixtures/yaml_with_symbols.yml') end.to raise_error Psych::DisallowedClass, /Symbol/ end it 'can override the value of permitted_symbols' do mash = Hashie::Mash.load('spec/fixtures/yaml_with_symbols.yml', permitted_classes: [Symbol], permitted_symbols: %i[ user_icon width height ]) expect(mash.user_icon.width).to eq(200) end it 'raises an error on insufficient permitted_symbols' do expect do Hashie::Mash.load('spec/fixtures/yaml_with_symbols.yml', permitted_classes: [Symbol], permitted_symbols: %i[ user_icon width ]) end.to raise_error Psych::DisallowedClass, /Symbol/ end end end describe '#to_module(mash_method_name)' do let(:mash) { described_class.new } subject { Class.new.extend mash.to_module } it 'defines a settings method on the klass class that extends the module' do expect(subject).to respond_to(:settings) expect(subject.settings).to eq mash end context 'when a settings_method_name is set' do let(:mash_method_name) { 'config' } subject { Class.new.extend mash.to_module(mash_method_name) } it 'defines a settings method on the klass class that extends the module' do expect(subject).to respond_to(mash_method_name.to_sym) expect(subject.send(mash_method_name.to_sym)).to eq mash end end end describe '#reverse_merge' do subject { described_class.new(a: 1, b: 2) } it 'unifies strings and symbols' do expect(subject.reverse_merge(a: 2).length).to eq 2 expect(subject.reverse_merge('a' => 2).length).to eq 2 end it 'does not overwrite values' do expect(subject.reverse_merge(a: 5).a).to eq subject.a end context 'when using with subclass' do let(:subclass) { Class.new(Hashie::Mash) } subject { subclass.new(a: 1) } it 'creates an instance of subclass' do expect(subject.reverse_merge(a: 5)).to be_kind_of(subclass) end end end describe '#invert' do subject(:mash) { described_class.new(a: 'apple', b: 4) } it 'returns a Hashie::Mash' do expect(mash.invert).to be_kind_of(described_class) end it 'returns a mash with the keys and values inverted' do expect(mash.invert).to eq('apple' => 'a', 4 => 'b') end context 'when using with subclass' do let(:subclass) { Class.new(Hashie::Mash) } subject(:sub_mash) { subclass.new(a: 1, b: nil) } it 'creates an instance of subclass' do expect(sub_mash.invert).to be_kind_of(subclass) end end end describe '#reject' do subject(:mash) { described_class.new(a: 1, b: nil) } it 'returns a Hashie::Mash' do expect(mash.reject { |_k, v| v.nil? }).to be_kind_of(described_class) end it 'rejects keys for which the block returns true' do expect(mash.reject { |_k, v| v.nil? }).to eq('a' => 1) end context 'when using with subclass' do let(:subclass) { Class.new(Hashie::Mash) } subject(:sub_mash) { subclass.new(a: 1, b: nil) } it 'creates an instance of subclass' do expect(sub_mash.reject { |_k, v| v.nil? }).to be_kind_of(subclass) end end end describe '#select' do subject(:mash) { described_class.new(a: 'apple', b: 4) } it 'returns a Hashie::Mash' do expect(mash.select { |_k, v| v.is_a? String }).to be_kind_of(described_class) end it 'selects keys for which the block returns true' do expect(mash.select { |_k, v| v.is_a? String }).to eq('a' => 'apple') end context 'when using with subclass' do let(:subclass) { Class.new(Hashie::Mash) } subject(:sub_mash) { subclass.new(a: 1, b: nil) } it 'creates an instance of subclass' do expect(sub_mash.select { |_k, v| v.is_a? String }).to be_kind_of(subclass) end end end describe '.quiet' do it 'returns a subclass of the calling class' do expect(Hashie::Mash.quiet.new).to be_a(Hashie::Mash) end it 'memoizes and returns classes' do call_one = Hashie::Mash.quiet call_two = Hashie::Mash.quiet expect(Hashie::Mash.instance_variable_get('@memoized_classes').count).to eq(1) expect(call_one).to eq(call_two) end end with_minimum_ruby('2.3.0') do describe '#dig' do subject { described_class.new(a: { b: 1 }) } it 'accepts both string and symbol as key' do expect(subject.dig(:a, :b)).to eq(1) expect(subject.dig('a', 'b')).to eq(1) end context 'when the Mash wraps a Hashie::Array' do it 'handles digging into an array' do mash = described_class.new(alphabet: { first_three: Hashie::Array['a', 'b', 'c'] }) expect(mash.dig(:alphabet, :first_three, 0)).to eq 'a' end end end end with_minimum_ruby('2.4.0') do describe '#transform_values' do subject(:mash) { described_class.new(a: 1) } it 'returns a Hashie::Mash' do expect(mash.transform_values(&:to_s)).to be_kind_of(described_class) end it 'transforms the value' do expect(mash.transform_values(&:to_s).a).to eql('1') end context 'when using with subclass' do let(:subclass) { Class.new(Hashie::Mash) } subject(:sub_mash) { subclass.new(a: 1).transform_values { |a| a + 2 } } it 'creates an instance of subclass' do expect(sub_mash).to be_kind_of(subclass) end end end describe '#compact' do subject(:mash) { described_class.new(a: 1, b: nil) } it 'returns a Hashie::Mash' do expect(mash.compact).to be_kind_of(described_class) end it 'removes keys with nil values' do expect(mash.compact).to eq('a' => 1) end context 'when using with subclass' do let(:subclass) { Class.new(Hashie::Mash) } subject(:sub_mash) { subclass.new(a: 1, b: nil) } it 'creates an instance of subclass' do expect(sub_mash.compact).to be_kind_of(subclass) end end end end with_minimum_ruby('2.5.0') do describe '#slice' do subject(:mash) { described_class.new(a: 1, b: 2) } it 'returns a Hashie::Mash' do expect(mash.slice(:a)).to be_kind_of(described_class) end it 'returns a Mash with only the keys passed' do expect(mash.slice(:a).to_hash).to eq('a' => 1) end context 'when using with subclass' do let(:subclass) { Class.new(Hashie::Mash) } subject(:sub_mash) { subclass.new(a: 1, b: 2) } it 'creates an instance of subclass' do expect(sub_mash.slice(:a)).to be_kind_of(subclass) end end end describe '#transform_keys' do subject(:mash) { described_class.new(a: 1, b: 2) } it 'returns a Hashie::Mash' do expect(mash.transform_keys { |k| k + k }).to be_kind_of(described_class) end it 'returns a Mash with transformed keys' do expect(mash.transform_keys { |k| k + k }).to eq('aa' => 1, 'bb' => 2) end context 'when using with subclass' do let(:subclass) { Class.new(Hashie::Mash) } subject(:sub_mash) { subclass.new(a: 1, b: 2) } it 'creates an instance of subclass' do expect(sub_mash.transform_keys { |k| k + k }).to be_kind_of(subclass) end end end end with_minimum_ruby('2.6.0') do context 'ruby 2.6 merging' do subject(:mash) { Hashie::Mash.new(model: 'Honda') } it 'merges multiple hashes and mashes passeed to #merge' do first_hash = { model: 'Ford' } second_hash = { model: 'DeLorean' } expect(mash.merge(first_hash, second_hash)).to eq('model' => 'DeLorean') end end end with_minimum_ruby('3.0.0') do context '#except' do subject(:mash) { described_class.new(a: 'A', b: 'B') } it 'return a Hashie::Mash' do expect(mash.except(:b)).to be_kind_of(described_class) end it 'excludes keys' do expect(mash.except(:b)).to eq('a' => 'A') end end end end hashie-5.0.0/spec/hashie/parsers/000077500000000000000000000000001414227716500166405ustar00rootroot00000000000000hashie-5.0.0/spec/hashie/parsers/yaml_erb_parser_spec.rb000066400000000000000000000020701414227716500233440ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Extensions::Parsers::YamlErbParser do describe '.perform' do context 'a file' do let(:config) do <<-CONFIG --- foo: verbatim bar: <%= "erb" %> baz: "<%= __FILE__ %>" CONFIG end let(:path) { 'template.yml' } subject { described_class.new(path).perform } before do expect(File).to receive(:read).with(path).and_return(config) end it { is_expected.to be_a(Hash) } it 'parses YAML after interpolating ERB' do expect(subject['foo']).to eq 'verbatim' expect(subject['bar']).to eq 'erb' expect(subject['baz']).to eq path end end context 'Pathname' do let(:tempfile) do file = Tempfile.new(['foo', '.yml']) file.write("---\nfoo: hello\n") file.rewind file end subject { described_class.new(Pathname(tempfile.path)) } it '"#perform" can be done in case of path is a Pathname object.' do expect(subject.perform).to eq 'foo' => 'hello' end end end end hashie-5.0.0/spec/hashie/rash_spec.rb000066400000000000000000000044071414227716500174620ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Rash do subject do Hashie::Rash.new( /hello/ => 'hello', /world/ => 'world', 'other' => 'whee', true => false, 1 => 'awesome', 1..1000 => 'rangey', /(bcd)/ => proc { |m| m[1] } # /.+/ => "EVERYTHING" ) end it 'finds strings' do expect(subject['other']).to eq 'whee' expect(subject['well hello there']).to eq 'hello' expect(subject['the world is round']).to eq 'world' expect(subject.all('hello world').sort).to eq %w[hello world] end it 'finds regexps' do expect(subject[/other/]).to eq 'whee' end it 'finds other objects' do expect(subject[true]).to eq false expect(subject[1]).to eq 'awesome' end it 'finds numbers from ranges' do expect(subject[250]).to eq 'rangey' expect(subject[999]).to eq 'rangey' expect(subject[1000]).to eq 'rangey' expect(subject[1001]).to be_nil end it 'finds floats from ranges' do expect(subject[10.1]).to eq 'rangey' expect(subject[1.0]).to eq 'rangey' expect(subject[1000.1]).to be_nil end it 'evaluates proc values' do expect(subject['abcdef']).to eq 'bcd' expect(subject['ffffff']).to be_nil end it 'finds using the find method' do expect(subject.fetch(10.1)).to eq 'rangey' expect(subject.fetch(true)).to be false end it 'raises in find unless a key matches' do expect { subject.fetch(1_000_000) }.to raise_error(KeyError) end it 'yields in find unless a key matches' do expect { |y| subject.fetch(1_000_000, &y) }.to yield_control expect { |y| subject.fetch(10.1, &y) }.to_not yield_control end it 'gives a default value' do expect(subject.fetch(10.1, 'noop')).to eq 'rangey' expect(subject.fetch(1_000_000, 'noop')).to eq 'noop' expect(subject.fetch(1_000_000) { 'noop' }).to eq 'noop' expect(subject.fetch(1_000_000) { |k| k }).to eq 1_000_000 expect(subject.fetch(1_000_000, 'noop') { 'op' }).to eq 'op' end it 'responds to hash methods' do expect(subject.respond_to?(:to_a)).to be true expect(subject.methods).to_not include(:to_a) end it 'does not lose keys' do subject.optimize_every = 1 expect(subject['hello']).to eq('hello') expect(subject['world']).to eq('world') end end hashie-5.0.0/spec/hashie/trash_spec.rb000066400000000000000000000255071414227716500176520ustar00rootroot00000000000000require 'spec_helper' describe Hashie::Trash do class TrashTest < Hashie::Trash property :first_name, from: :firstName end let(:trash) { TrashTest.new } describe 'translating properties' do it 'adds the property to the list' do expect(TrashTest.properties).to include(:first_name) end it 'creates a method for reading the property' do expect(trash).to respond_to(:first_name) end it 'creates a method for writing the property' do expect(trash).to respond_to(:first_name=) end it 'creates a method for writing the translated property' do expect(trash).to respond_to(:firstName=) end it 'does not create a method for reading the translated property' do expect(trash).not_to respond_to(:firstName) end it 'maintains translations hash mapping from the original to the translated name' do expect(TrashTest.translations[:firstName]).to eq(:first_name) end it 'maintains inverse translations hash mapping from the translated to the original name' do expect(TrashTest.inverse_translations[:first_name]).to eq :firstName end it '#permitted_input_keys contain the :from key of properties with translations' do expect(TrashTest.permitted_input_keys).to include :firstName end end describe 'standard properties' do class TrashTestPermitted < Hashie::Trash property :id end it '#permitted_input_keys contain names of properties without translations' do expect(TrashTestPermitted.permitted_input_keys).to include :id end end describe 'writing to properties' do it 'does not write to a non-existent property using []=' do expect { trash['abc'] = 123 }.to raise_error(NoMethodError) end it 'writes to an existing property using []=' do expect { trash[:first_name] = 'Bob' }.not_to raise_error expect(trash.first_name).to eq('Bob') expect { trash['first_name'] = 'John' }.to raise_error(NoMethodError) end it 'writes to a translated property using []=' do expect { trash[:firstName] = 'Bob' }.not_to raise_error expect { trash['firstName'] = 'Bob' }.to raise_error(NoMethodError) end it 'reads/writes to an existing property using a method call' do trash.first_name = 'Franklin' expect(trash.first_name).to eq 'Franklin' end it 'writes to an translated property using a method call' do trash.firstName = 'Franklin' expect(trash.first_name).to eq 'Franklin' end it 'writes to a translated property using #replace' do trash.replace(firstName: 'Franklin') expect(trash.first_name).to eq 'Franklin' end it 'writes to a non-translated property using #replace' do trash.replace(first_name: 'Franklin') expect(trash.first_name).to eq 'Franklin' end end describe ' initializing with a Hash' do it 'does not initialize non-existent properties' do expect { TrashTest.new(bork: 'abc') }.to raise_error(NoMethodError) end it 'sets the desired properties' do expect(TrashTest.new(first_name: 'Michael').first_name).to eq 'Michael' end context 'with both the translated property and the property' do it 'sets the desired properties' do expect(TrashTest.new(first_name: 'Michael', firstName: 'Maeve').first_name).to eq 'Michael' end end it 'sets the translated properties' do expect(TrashTest.new(firstName: 'Michael').first_name).to eq 'Michael' end end describe 'translating properties using a proc' do class TrashLambdaTest < Hashie::Trash property :first_name, from: :firstName, with: ->(value) { value.reverse } end let(:lambda_trash) { TrashLambdaTest.new } it 'translates the value given on initialization with the given lambda' do expect(TrashLambdaTest.new(firstName: 'Michael').first_name).to eq 'Michael'.reverse end it 'does not translate the value if given with the right property' do expect(TrashTest.new(first_name: 'Michael').first_name).to eq 'Michael' end it 'translates the value given as property with the given lambda' do lambda_trash.firstName = 'Michael' expect(lambda_trash.first_name).to eq 'Michael'.reverse end it 'does not translate the value given as right property' do lambda_trash.first_name = 'Michael' expect(lambda_trash.first_name).to eq 'Michael' end end describe 'translating multiple properties using a proc' do class SomeDataModel < Hashie::Trash property :value_a, from: :config, with: ->(config) { config.a } property :value_b, from: :config, with: ->(config) { config.b } end ConfigDataModel = Struct.new(:a, :b) subject { SomeDataModel.new(config: ConfigDataModel.new('value in a', 'value in b')) } it 'translates the first key' do expect(subject.value_a).to eq 'value in a' end it 'translates the second key' do expect(subject.value_b).to eq 'value in b' end it 'maintains translations hash mapping from the original to the translated name' do expect(SomeDataModel.translations).to eq(config: %i[value_a value_b]) end end describe 'translating multiple properties from the same source hash key' do class AnotherDataModel < Hashie::Trash property :first_name, transform_with: ->(n) { n.upcase } property :first_name_short, from: :first_name, transform_with: ->(n) { n[0, 3] } end subject { AnotherDataModel.new(first_name: 'Cathy') } it 'translates the first key with the given lambda' do expect(subject.first_name).to eq('CATHY') end it 'translates the second key with the given lambda and the initial value of the first key' do expect(subject.first_name_short).to eq('Cat') end end describe 'uses with or transform_with interchangeably' do class TrashLambdaTestTransformWith < Hashie::Trash property :first_name, from: :firstName, transform_with: ->(value) { value.reverse } end let(:lambda_trash) { TrashLambdaTestTransformWith.new } it 'translates the value given as property with the given lambda' do lambda_trash.firstName = 'Michael' expect(lambda_trash.first_name).to eq 'Michael'.reverse end it 'does not translate the value given as right property' do lambda_trash.first_name = 'Michael' expect(lambda_trash.first_name).to eq 'Michael' end end describe 'translating properties without from option using a proc' do class TrashLambdaTestWithProperties < Hashie::Trash property :first_name, transform_with: ->(value) { value.reverse } end let(:lambda_trash) { TrashLambdaTestWithProperties.new } it 'translates the value given as property with the given lambda' do lambda_trash.first_name = 'Michael' expect(lambda_trash.first_name).to eq 'Michael'.reverse end it 'transforms the value when given in constructor' do expect( TrashLambdaTestWithProperties.new(first_name: 'Michael').first_name ).to eq 'Michael'.reverse end context 'when :from option is given' do class TrashLambdaTest3 < Hashie::Trash property :first_name, from: :firstName, transform_with: ->(value) { value.reverse } end it 'does not override the :from option in the constructor' do expect(TrashLambdaTest3.new(first_name: 'Michael').first_name).to eq 'Michael' end it 'does not override the :from option when given as property' do t = TrashLambdaTest3.new t.first_name = 'Michael' expect(t.first_name).to eq 'Michael' end end end describe 'inheritable transforms' do class TransformA < Hashie::Trash property :some_value, transform_with: ->(v) { v.to_i } end class TransformB < TransformA property :some_other_value, transform_with: ->(v) { v.to_i } end class TransformC < TransformB property :some_value, transform_with: ->(v) { -v.to_i } end it 'inherit properties transforms' do expect(TransformB.new(some_value: '123', some_other_value: '456').some_value).to eq(123) end it 'replaces property transform' do expect(TransformC.new(some_value: '123', some_other_value: '456').some_value).to eq(-123) end end describe 'inheritable translations' do class TranslationA < Hashie::Trash property :some_value, from: 'someValue', with: ->(v) { v.to_i } end class TranslationB < TranslationA property :some_other_value, from: 'someOtherValue' end it 'inherit properties translations' do expect(TranslationB.new('someValue' => '123').some_value).to eq(123) end end it 'raises an error when :from have the same value as property' do expect do class WrongTrash < Hashie::Trash property :first_name, from: :first_name end end.to raise_error(ArgumentError) end context 'when subclassing' do class Person < Hashie::Trash property :first_name, from: :firstName end class Hobbit < Person; end subject { Hobbit.new(firstName: 'Frodo') } it 'keeps translation definitions in subclasses' do expect(subject.first_name).to eq('Frodo') end end context 'when copying properties from other properties' do it 'retains the original and also sets the copy' do simple = Class.new(Hashie::Trash) do property :id property :copy_of_id, from: :id end subject = simple.new(id: 1) expect(subject.id).to eq(1) expect(subject.copy_of_id).to eq(1) end it 'grabs the default for the original if it is not set' do with_default = Class.new(Hashie::Trash) do property :id, default: 0 property :copy_of_id, from: :id end subject = with_default.new expect(subject.id).to eq(0) expect(subject.copy_of_id).to eq(0) end it 'can be a required value' do with_required = Class.new(Hashie::Trash) do property :id property :copy_of_id, from: :id, required: true, message: 'must be set' end expect { with_required.new }.to raise_error( ArgumentError, "The property 'copy_of_id' must be set" ) end it 'does not set properties that do not exist' do from_non_property = Class.new(Hashie::Trash) do property :copy_of_value, from: :value end subject = from_non_property.new(value: 0) expect(subject).not_to respond_to(:value) expect { subject[:value] }.to raise_error( NoMethodError, "The property 'value' is not defined for ." ) expect(subject.to_h[:value]).to eq(nil) expect(subject.copy_of_value).to eq(0) end it 'is not order-dependent in definition' do simple = Class.new(Hashie::Trash) do property :copy_of_id, from: :id property :id end subject = simple.new(id: 1) expect(subject.id).to eq(1) expect(subject.copy_of_id).to eq(1) end end end hashie-5.0.0/spec/hashie/utils_spec.rb000066400000000000000000000011611414227716500176570ustar00rootroot00000000000000require 'spec_helper' def a_method_to_match_against 'Hello world!' end RSpec.describe Hashie::Utils do describe '.method_information' do it 'states the module or class that a native method was defined in' do bound_method = method(:trust) message = Hashie::Utils.method_information(bound_method) expect(message).to match('Kernel') end it 'states the line a Ruby method was defined at' do bound_method = method(:a_method_to_match_against) message = Hashie::Utils.method_information(bound_method) expect(message).to match('spec/hashie/utils_spec.rb') end end end hashie-5.0.0/spec/hashie/version_spec.rb000066400000000000000000000001661414227716500202100ustar00rootroot00000000000000require 'spec_helper' describe Hashie do it 'has a version' do expect(Hashie::VERSION).not_to be_nil end end hashie-5.0.0/spec/hashie_spec.rb000066400000000000000000000004111414227716500165140ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Hashie do describe '.logger' do include_context 'with a logger' it 'is available via an accessor' do Hashie.logger.info('Fee fi fo fum') expect(logger_output).to match('Fee fi fo fum') end end end hashie-5.0.0/spec/integration/000077500000000000000000000000001414227716500162435ustar00rootroot00000000000000hashie-5.0.0/spec/integration/active_support/000077500000000000000000000000001414227716500213125ustar00rootroot00000000000000hashie-5.0.0/spec/integration/active_support/Gemfile000066400000000000000000000007241414227716500226100ustar00rootroot00000000000000source 'http://rubygems.org' gem 'hashie', path: '../../..' require File.expand_path('../../../../lib/hashie/extensions/ruby_version', __FILE__) # rubocop:disable Bundler/DuplicatedGem if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >= Hashie::Extensions::RubyVersion.new('2.4.0') gem 'activesupport', '~> 5.x', require: false else gem 'activesupport', '~> 4.x', require: false end # rubocop:enable Bundler/DuplicatedGem gem 'rake' gem 'rspec', '~> 3.5.0' hashie-5.0.0/spec/integration/active_support/integration_spec.rb000066400000000000000000000234751414227716500252070ustar00rootroot00000000000000require 'active_support' require 'active_support/core_ext' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash' require 'hashie' RSpec.configure do |config| config.expect_with :rspec do |expect| expect.syntax = :expect end end RSpec.describe Hashie::Mash do describe '#extractable_options?' do subject { Hashie::Mash.new(name: 'foo') } let(:args) { [101, 'bar', subject] } it 'can be extracted from an array' do expect(args.extract_options!).to eq subject expect(args).to eq [101, 'bar'] end end end RSpec.describe Hashie::Extensions::DeepFind do let(:hash) do { library: { books: [ { title: 'Call of the Wild' }, { title: 'Moby Dick' } ], shelves: nil, location: { address: '123 Library St.', title: 'Main Library' } } } end subject(:instance) { hash.with_indifferent_access.extend(Hashie::Extensions::DeepFind) } describe '#deep_find' do it 'indifferently detects a value from a nested hash' do expect(instance.deep_find(:address)).to eq('123 Library St.') expect(instance.deep_find('address')).to eq('123 Library St.') end it 'indifferently detects a value from a nested array' do expect(instance.deep_find(:title)).to eq('Call of the Wild') expect(instance.deep_find('title')).to eq('Call of the Wild') end it 'indifferently returns nil if it does not find a match' do expect(instance.deep_find(:wahoo)).to be_nil expect(instance.deep_find('wahoo')).to be_nil end end describe '#deep_find_all' do it 'indifferently detects all values from a nested hash' do expect(instance.deep_find_all(:title)) .to eq(['Call of the Wild', 'Moby Dick', 'Main Library']) expect(instance.deep_find_all('title')) .to eq(['Call of the Wild', 'Moby Dick', 'Main Library']) end it 'indifferently returns nil if it does not find any matches' do expect(instance.deep_find_all(:wahoo)).to be_nil expect(instance.deep_find_all('wahoo')).to be_nil end end end RSpec.describe Hashie::Extensions::DeepLocate do let(:hash) do { from: 0, size: 25, query: { bool: { must: [ { query_string: { query: 'foobar', default_operator: 'AND', fields: [ 'title^2', '_all' ] } }, { match: { field_1: 'value_1' } }, { range: { lsr09: { gte: 2014 } } } ], should: [ { match: { field_2: 'value_2' } } ], must_not: [ { range: { lsr10: { gte: 2014 } } } ] } } } end describe '#deep_locate' do subject(:instance) { hash.with_indifferent_access.extend(described_class) } it 'can locate symbolic keys' do expect(described_class.deep_locate(:lsr10, instance)).to eq ['lsr10' => { 'gte' => 2014 }] end it 'can locate string keys' do expect(described_class.deep_locate('lsr10', instance)).to eq ['lsr10' => { 'gte' => 2014 }] end end end RSpec.describe Hashie::Extensions::IndifferentAccess do class Initializable attr_reader :coerced, :value def initialize(obj, coerced = nil) @coerced = coerced @value = obj.class.to_s end def coerced? !@coerced.nil? end end class Coercable < Initializable def self.coerce(obj) new(obj, true) end end class IndifferentHashWithMergeInitializer < Hash include Hashie::Extensions::MergeInitializer include Hashie::Extensions::IndifferentAccess class << self alias build new end end class IndifferentHashWithArrayInitializer < Hash include Hashie::Extensions::IndifferentAccess class << self alias build [] end end class IndifferentHashWithTryConvertInitializer < Hash include Hashie::Extensions::IndifferentAccess class << self alias build try_convert end end class CoercableHash < Hash include Hashie::Extensions::Coercion include Hashie::Extensions::MergeInitializer end class MashWithIndifferentAccess < Hashie::Mash include Hashie::Extensions::IndifferentAccess end shared_examples_for 'hash with indifferent access' do it 'is able to access via string or symbol' do indifferent_hash = ActiveSupport::HashWithIndifferentAccess.new(abc: 123) h = subject.build(indifferent_hash) expect(h[:abc]).to eq 123 expect(h['abc']).to eq 123 end describe '#values_at' do it 'indifferently finds values' do indifferent_hash = ActiveSupport::HashWithIndifferentAccess.new( :foo => 'bar', 'baz' => 'qux' ) h = subject.build(indifferent_hash) expect(h.values_at('foo', :baz)).to eq %w[bar qux] end end describe '#fetch' do it 'works like normal fetch, but indifferent' do indifferent_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: 'bar') h = subject.build(indifferent_hash) expect(h.fetch(:foo)).to eq h.fetch('foo') expect(h.fetch(:foo)).to eq 'bar' end end describe '#delete' do it 'deletes indifferently' do indifferent_hash = ActiveSupport::HashWithIndifferentAccess.new( :foo => 'bar', 'baz' => 'qux' ) h = subject.build(indifferent_hash) h.delete('foo') h.delete(:baz) expect(h).to be_empty end end describe '#key?' do let(:h) do indifferent_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: 'bar') subject.build(indifferent_hash) end it 'finds it indifferently' do expect(h).to be_key(:foo) expect(h).to be_key('foo') end %w[include? member? has_key?].each do |key_alias| it "is aliased as #{key_alias}" do expect(h.send(key_alias.to_sym, :foo)).to be(true) expect(h.send(key_alias.to_sym, 'foo')).to be(true) end end end describe '#update' do let(:h) do indifferent_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: 'bar') subject.build(indifferent_hash) end it 'allows keys to be indifferent still' do h.update(baz: 'qux') expect(h['foo']).to eq 'bar' expect(h['baz']).to eq 'qux' end it 'recursively injects indifference into sub-hashes' do h.update(baz: { qux: 'abc' }) expect(h['baz']['qux']).to eq 'abc' end it 'does not change the ancestors of the injected object class' do h.update(baz: { qux: 'abc' }) expect({}).not_to be_respond_to(:indifferent_access?) end end describe '#replace' do let(:h) do indifferent_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: 'bar') subject.build(indifferent_hash).replace(bar: 'baz', hi: 'bye') end it 'returns self' do expect(h).to be_a(subject) end it 'removes old keys' do [:foo, 'foo'].each do |k| expect(h[k]).to be_nil expect(h.key?(k)).to be_falsy end end it 'creates new keys with indifferent access' do [:bar, 'bar', :hi, 'hi'].each { |k| expect(h.key?(k)).to be_truthy } expect(h[:bar]).to eq 'baz' expect(h['bar']).to eq 'baz' expect(h[:hi]).to eq 'bye' expect(h['hi']).to eq 'bye' end end describe '#try_convert' do describe 'with conversion' do let(:h) do indifferent_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: 'bar') subject.try_convert(indifferent_hash) end it 'is a subject' do expect(h).to be_a(subject) end end describe 'without conversion' do let(:h) { subject.try_convert('{ :foo => bar }') } it 'is nil' do expect(h).to be_nil end end end end describe 'with merge initializer' do subject { IndifferentHashWithMergeInitializer } it_should_behave_like 'hash with indifferent access' end describe 'with array initializer' do subject { IndifferentHashWithArrayInitializer } it_should_behave_like 'hash with indifferent access' end describe 'with try convert initializer' do subject { IndifferentHashWithTryConvertInitializer } it_should_behave_like 'hash with indifferent access' end describe 'with coercion' do subject { CoercableHash } let(:instance) { subject.new } it 'supports coercion for ActiveSupport::HashWithIndifferentAccess' do subject.coerce_key :foo, ActiveSupport::HashWithIndifferentAccess.new(Coercable => Coercable) instance[:foo] = { 'bar_key' => 'bar_value', 'bar2_key' => 'bar2_value' } expect(instance[:foo].keys).to all(be_coerced) expect(instance[:foo].values).to all(be_coerced) expect(instance[:foo]).to be_a(ActiveSupport::HashWithIndifferentAccess) end end describe 'Mash with indifferent access' do it 'is able to be created for a deep nested HashWithIndifferentAccess' do indifferent_hash = ActiveSupport::HashWithIndifferentAccess.new(abc: { def: 123 }) MashWithIndifferentAccess.new(indifferent_hash) end end class DashTestDefaultProc < Hashie::Dash property :fields, default: -> { [] } end describe DashTestDefaultProc do it 'as_json behaves correctly with default proc' do object = described_class.new expect(object.as_json).to be == { 'fields' => [] } end end end hashie-5.0.0/spec/integration/elasticsearch/000077500000000000000000000000001414227716500210555ustar00rootroot00000000000000hashie-5.0.0/spec/integration/elasticsearch/Gemfile000066400000000000000000000002521414227716500223470ustar00rootroot00000000000000source 'http://rubygems.org' gem 'elasticsearch-api', '~> 7.0.0' gem 'elasticsearch-model', '~> 7.0.0' gem 'hashie', path: '../../..' gem 'rake' gem 'rspec', '~> 3.5.0' hashie-5.0.0/spec/integration/elasticsearch/integration_spec.rb000066400000000000000000000021731414227716500247420ustar00rootroot00000000000000require 'elasticsearch/model' require 'hashie' RSpec.configure do |config| config.expect_with :rspec do |expect| expect.syntax = :expect end end class MyModel < Hashie::Mash include Elasticsearch::Model disable_warnings index_name 'model' document_type 'model' def as_indexed_json(options = {}) { body: '{}' }.merge(options) end end RSpec.describe 'elaasticsearch-model' do # See https://github.com/hashie/hashie/issues/354#issuecomment-363306114 # for the reason why this doesn't work as you would expect it 'raises an error when the model does not have an id' do object = MyModel.new stub_elasticsearch_client expect { object.__elasticsearch__.index_document }.to raise_error(NameError) end it 'does not raise an error when the model has an id' do object = MyModel.new(id: 123) stub_elasticsearch_client expect { object.__elasticsearch__.index_document }.not_to raise_error end def stub_elasticsearch_client response = double('Response', body: '{}') allow_any_instance_of(Elasticsearch::Transport::Client).to\ receive(:perform_request) { response } end end hashie-5.0.0/spec/integration/omniauth-oauth2/000077500000000000000000000000001414227716500212675ustar00rootroot00000000000000hashie-5.0.0/spec/integration/omniauth-oauth2/.rspec000066400000000000000000000000641414227716500224040ustar00rootroot00000000000000--colour --format=documentation --pattern=*_spec.rb hashie-5.0.0/spec/integration/omniauth-oauth2/Gemfile000066400000000000000000000002601414227716500225600ustar00rootroot00000000000000source 'http://rubygems.org' gem 'hashie', path: '../../..' gem 'omniauth' gem 'omniauth-oauth2', '~> 1.4.0' gem 'rails', '~> 5.0.1' gem 'rspec', '~> 3.5.0' gem 'rspec-rails' hashie-5.0.0/spec/integration/omniauth-oauth2/app.rb000066400000000000000000000020201414227716500223660ustar00rootroot00000000000000require 'action_controller/railtie' require 'action_view/railtie' require 'action_view/testing/resolvers' require 'rails/test_unit/railtie' require_relative 'some_site' module RailsApp class Application < ::Rails::Application config.eager_load = false config.secret_key_base = 'hashieintegrationtest' config.middleware.use OmniAuth::Builder do provider :some_site end routes.append do get '/' => 'application#index' end end end LAYOUT = <<-HTML.freeze TestApp <%= csrf_meta_tags %> <%= yield %> HTML INDEX = '

Hello, world!

'.freeze class ApplicationController < ActionController::Base include Rails.application.routes.url_helpers layout 'application' self.view_paths = [ActionView::FixtureResolver.new( 'layouts/application.html.erb' => LAYOUT, 'application/index.html.erb' => INDEX )] def index; end end Bundler.require(:default, Rails.env) RailsApp::Application.initialize! hashie-5.0.0/spec/integration/omniauth-oauth2/integration_spec.rb000066400000000000000000000011321414227716500251460ustar00rootroot00000000000000ENV['RAILS_ENV'] = 'test' require 'rspec/core' RSpec.describe 'omniauth-oauth2 inside of rails', type: :request do let(:stdout) { StringIO.new } around(:each) do |example| original_stdout = $stdout $stdout = stdout require_relative 'app' require 'rspec/rails' example.run $stdout = original_stdout end it 'does not log anything to STDOUT when initializing a Rails app and is set to Rails logger' do expect(stdout.string).to eq('') expect(Hashie.logger).to eq(Rails.logger) end it 'works' do get '/' assert_select 'h1', 'Hello, world!' end end hashie-5.0.0/spec/integration/omniauth-oauth2/some_site.rb000066400000000000000000000017061414227716500236070ustar00rootroot00000000000000require 'omniauth-oauth2' module OmniAuth module Strategies class SomeSite < OmniAuth::Strategies::OAuth2 # Give your strategy a name. option :name, 'some_site' # This is where you pass the options you would pass when # initializing your consumer from the OAuth gem. option :client_options, site: 'https://api.somesite.com' # These are called after authentication has succeeded. If # possible, you should try to set the UID without making # additional calls (if the user id is returned with the token # or as a URI parameter). This may not be possible with all # providers. uid { raw_info['id'] } info do { name: raw_info['name'], email: raw_info['email'] } end extra do { 'raw_info' => raw_info } end def raw_info @raw_info ||= access_token.get('/me').parsed end end end end hashie-5.0.0/spec/integration/omniauth/000077500000000000000000000000001414227716500200675ustar00rootroot00000000000000hashie-5.0.0/spec/integration/omniauth/.rspec000066400000000000000000000000641414227716500212040ustar00rootroot00000000000000--colour --format=documentation --pattern=*_spec.rb hashie-5.0.0/spec/integration/omniauth/Gemfile000066400000000000000000000002551414227716500213640ustar00rootroot00000000000000source 'http://rubygems.org' gem 'benchmark-ips' gem 'hashie', path: '../../..' gem 'omniauth' gem 'rack-test', '~> 0.6.3' gem 'rake' gem 'rspec', '~> 3.5.0' gem 'sinatra' hashie-5.0.0/spec/integration/omniauth/Rakefile000066400000000000000000000013411414227716500215330ustar00rootroot00000000000000namespace :perf do task :setup do require 'omniauth' require 'rack/test' app = Rack::Builder.new do |b| b.use Rack::Session::Cookie, secret: 'abc123' b.use OmniAuth::Strategies::Developer b.run ->(_env) { [200, {}, ['Not Found']] } end.to_app @app = Rack::MockRequest.new(app) def call_app(path = ENV['GET_PATH'] || '/') result = @app.get(path) raise "Did not succeed #{result.body}" unless result.status == 200 result end end task ips: :setup do require 'benchmark/ips' Benchmark.ips do |x| x.hold!('../../../tmp/omniauth_benchmark.json') x.report('before') { call_app } x.report('after') { call_app } x.compare! end end end hashie-5.0.0/spec/integration/omniauth/app.rb000066400000000000000000000003321414227716500211720ustar00rootroot00000000000000require 'sinatra' require 'omniauth' class MyApplication < Sinatra::Base use Rack::Session::Cookie, secret: 'hashie integration tests' use OmniAuth::Strategies::Developer get '/' do 'Hello World' end end hashie-5.0.0/spec/integration/omniauth/integration_spec.rb000066400000000000000000000012621414227716500237520ustar00rootroot00000000000000ENV['RACK_ENV'] = 'test' require 'rspec/core' require 'rack/test' RSpec.configure do |config| config.expect_with :rspec do |expect| expect.syntax = :expect end end RSpec.describe 'omniauth' do include Rack::Test::Methods def app MyApplication end let(:stdout) { StringIO.new } around(:each) do |example| original_stdout = $stdout $stdout = stdout require_relative 'app' example.run $stdout = original_stdout end it 'does not log anything to STDOUT when initializing' do expect(stdout.string).to eq('') end it 'works' do get '/' expect(last_response).to be_ok expect(last_response.body).to eq 'Hello World' end end hashie-5.0.0/spec/integration/rails-without-dependency/000077500000000000000000000000001414227716500231725ustar00rootroot00000000000000hashie-5.0.0/spec/integration/rails-without-dependency/.rspec000066400000000000000000000000641414227716500243070ustar00rootroot00000000000000--colour --format=documentation --pattern=*_spec.rb hashie-5.0.0/spec/integration/rails-without-dependency/Gemfile000066400000000000000000000001251414227716500244630ustar00rootroot00000000000000source 'http://rubygems.org' gem 'hashie', path: '../../..' gem 'rspec', '~> 3.5.0' hashie-5.0.0/spec/integration/rails-without-dependency/integration_spec.rb000066400000000000000000000005371414227716500270610ustar00rootroot00000000000000require 'rspec/core' RSpec.describe 'partial-rails' do context 'when Rails constant is present but the railties are not' do before(:all) do class Rails # A class about railways end end it 'does not raise an exception when we require hashie' do expect { require 'hashie' }.not_to raise_error end end end hashie-5.0.0/spec/integration/rails/000077500000000000000000000000001414227716500173555ustar00rootroot00000000000000hashie-5.0.0/spec/integration/rails/.rspec000066400000000000000000000000641414227716500204720ustar00rootroot00000000000000--colour --format=documentation --pattern=*_spec.rb hashie-5.0.0/spec/integration/rails/Gemfile000066400000000000000000000001771414227716500206550ustar00rootroot00000000000000source 'http://rubygems.org' gem 'hashie', path: '../../..' gem 'rails', '~> 6.0.0' gem 'rspec', '~> 3.5.0' gem 'rspec-rails' hashie-5.0.0/spec/integration/rails/app.rb000066400000000000000000000013551414227716500204660ustar00rootroot00000000000000require 'action_controller/railtie' require 'action_view/railtie' require 'action_view/testing/resolvers' require 'rails/test_unit/railtie' module RailsApp class Application < ::Rails::Application config.eager_load = false config.secret_key_base = 'hashieintegrationtest' routes.append do get '/' => 'application#index' end end end PAGE = <<-HTML.freeze TestApp <%= csrf_meta_tags %>

Hello, world!

HTML class ApplicationController < ActionController::Base include Rails.application.routes.url_helpers def index render inline: PAGE end end Bundler.require(:default, Rails.env) RailsApp::Application.initialize! hashie-5.0.0/spec/integration/rails/integration_spec.rb000066400000000000000000000021211414227716500232330ustar00rootroot00000000000000ENV['RAILS_ENV'] = 'test' require 'rspec/core' RSpec.describe 'rails', type: :request do let(:stdout) { StringIO.new } around(:each) do |example| original_stdout = $stdout $stdout = stdout require_relative 'app' require 'rspec/rails' example.run $stdout = original_stdout end it 'does not log anything to STDOUT when initializing' do expect(stdout.string).to eq('') end it 'sets the Hashie logger to the Rails logger' do expect(Hashie.logger).to eq(Rails.logger) end context '#except' do subject { Hashie::Mash.new(x: 1, y: 2) } it 'returns an instance of the class it was called on' do class HashieKlass < Hashie::Mash; end hashie_klass = HashieKlass.new(subject) expect(hashie_klass.except('x')).to be_a HashieKlass end it 'works with string keys' do expect(subject.except('x')).to eq Hashie::Mash.new(y: 2) end it 'works with symbol keys' do expect(subject.except(:x)).to eq Hashie::Mash.new(y: 2) end end it 'works' do get '/' assert_select 'h1', 'Hello, world!' end end hashie-5.0.0/spec/spec_helper.rb000066400000000000000000000010131414227716500165310ustar00rootroot00000000000000if ENV['CI'] require 'simplecov' SimpleCov.start end require 'pry' require 'rspec' require 'hashie' require 'json' require 'rspec/pending_for' require './spec/support/ruby_version_check' require './spec/support/logger' require './spec/support/matchers' Dir[File.expand_path(File.join(__dir__, 'support', '**', '*'))].sort.each { |file| require file } RSpec.configure do |config| config.extend RubyVersionCheck config.expect_with :rspec do |expect| expect.syntax = :expect end config.warnings = true end hashie-5.0.0/spec/support/000077500000000000000000000000001414227716500154345ustar00rootroot00000000000000hashie-5.0.0/spec/support/integration_specs.rb000066400000000000000000000023121414227716500214770ustar00rootroot00000000000000# Generates the bundle command for running an integration test # # @param [String] integration the integration folder to run # @param [String] command the command to run # @return [String] def integration_command(integration, command) "#{integration_gemfile(integration)} #{command}" end # Generates the Gemfile for an integration # # @param [String] integration the integration test name # @return [String] def integration_gemfile(integration) "BUNDLE_GEMFILE=#{integration_path(integration)}/Gemfile" end # Generates the path to the integration # # @param [String] integration the integration test name # @return [String] def integration_path(integration) "spec/integration/#{integration}" end # Runs all integration specs in their own environment def run_all_integration_specs(handler: ->(_code) {}, logger: ->(_msg) {}) Dir['spec/integration/*'] .map { |directory| directory.split('/').last } .each do |integration| logger.call(%(Running "#{integration}" integration spec)) system(integration_command(integration, 'bundle --quiet')) system(integration_command(integration, "bundle exec rspec #{integration_path(integration)}")) handler.call($CHILD_STATUS.exitstatus) end end hashie-5.0.0/spec/support/logger.rb000066400000000000000000000011611414227716500172370ustar00rootroot00000000000000# A shared context that allows you to check the output of Hashie's logger. # # @example # include_context 'with a logger' # # it 'logs info message' do # Hashie.logger.info 'What is happening in here?!' # # expect(logger_output).to match('What is happening in here?!') # end RSpec.shared_context 'with a logger' do # @private let(:log) { StringIO.new } # The output string from the logger let(:logger_output) { log.rewind && log.string } around(:each) do |example| original_logger = Hashie.logger Hashie.logger = Logger.new(log) example.run Hashie.logger = original_logger end end hashie-5.0.0/spec/support/matchers.rb000066400000000000000000000003611414227716500175670ustar00rootroot00000000000000RSpec::Matchers.define :parse_as_valid_ruby do require 'ripper' match do |actual| parsed = Ripper.sexp(actual) !parsed.nil? end failure_message do |actual| "expected that #{actual} would parse as valid Ruby" end end hashie-5.0.0/spec/support/module_context.rb000066400000000000000000000003041414227716500210070ustar00rootroot00000000000000shared_context 'included hash module' do let!(:dummy_class) do klass = Class.new(::Hash) klass.send :include, described_class klass end subject do dummy_class.new end end hashie-5.0.0/spec/support/ruby_version_check.rb000066400000000000000000000002771414227716500216520ustar00rootroot00000000000000module RubyVersionCheck def with_minimum_ruby(version) yield if Hashie::Extensions::RubyVersion.new(RUBY_VERSION) >= Hashie::Extensions::RubyVersion.new(version) end end hashie-5.0.0/spec/support/shared_examples.rb000066400000000000000000000013611414227716500211260ustar00rootroot00000000000000RSpec.shared_examples 'Dash default handling' do |property, name = property| it 'uses the default when initializing' do expect(test.new(name => nil).public_send(property)).to eq '' end it 'allows you to set the value to nil with the hash writer' do trash = test.new(name => 'foo') trash[name] = nil expect(trash.public_send(property)).to be_nil end it 'allows you to set the value to nil with the method writer' do trash = test.new(name => 'foo') trash[name] = nil expect(trash.public_send(property)).to be_nil end it 'uses the default when updating with defaults' do trash = test.new(name => 'foo') trash.update_attributes!(name => nil) expect(trash.public_send(property)).to eq '' end end