diff --git a/.codeclimate.yml b/.codeclimate.yml index f871444b..f16a69c4 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,2 +1,22 @@ -exclude_paths: +exclude_patterns: - 'tasks/' +plugins: + # No to-dos or similar + fixme: + enabled: true + exclude_patterns: + - '.rubocop.*' + # ABC-complexity + flog: + enabled: true + config: + score_threshold: 25.0 + exclude_patterns: + - 'spec/' + # Markdown lint with rules from https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md + markdownlint: + enabled: true + # Ruby lint + rubocop: + enabled: true + channel: rubocop-1-50-2 diff --git a/.coveralls.yml b/.coveralls.yml index 91600595..29f1af09 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1 @@ -service_name: travis-ci +service_name: semaphore diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..db3011f2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,73 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: codeql + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + - cron: '45 10 * * 6' + +jobs: + analyze: + name: analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'ruby' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: /language:${{matrix.language}} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..666d2083 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,23 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: dependency-review +on: + pull_request: + branches: + - master + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Dependency Review + uses: actions/dependency-review-action@v2 diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 00000000..8ea0c3be --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,82 @@ +name: build + +on: + push: + branches: + - master + pull_request: + branches: + - master + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2.2 + bundler-cache: true + - name: Run rubocop + run: bundle exec rubocop + spec: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: + - 3.2.2 + - 3.2.1 + - 3.2.0 + - 3.1.4 + - 3.1.3 + - 3.1.2 + - 3.1.1 + - 3.1.0 + - 3.0.6 + - 3.0.5 + - 3.0.4 + - 3.0.3 + - 3.0.2 + - 3.0.1 + - 3.0.0 + - 2.7.8 + - 2.7.7 + - 2.7.6 + - 2.7.5 + - 2.7.4 + - 2.7.3 + - 2.7.2 + - 2.7.1 + - 2.7.0 + steps: + - uses: actions/checkout@v3 + - name: Set up ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - name: Run rspec + run: bundle exec rspec + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2.2 + bundler-cache: true + - name: Report rspec test coverage to coveralls.io + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + run: bundle exec rspec + - name: Report rspec test coverage to codeclimate.com + uses: paambaati/codeclimate-action@v4.0.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: bundle exec rspec diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 00000000..5b0c0c33 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,25 @@ +name: semgrep +on: + workflow_dispatch: {} + pull_request: {} + push: + branches: + - main + - master + paths: + - .github/workflows/semgrep.yml + schedule: + # random HH:MM to avoid a load spike on GitHub Actions at 00:00 + - cron: '42 4 * * *' +jobs: + semgrep: + name: semgrep + runs-on: ubuntu-20.04 + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + container: + image: returntocorp/semgrep + if: (github.actor != 'dependabot[bot]') + steps: + - uses: actions/checkout@v3 + - run: semgrep ci diff --git a/.gitignore b/.gitignore index ec8b18a6..1ebba6fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -# Vim +# Vim / IDEs *.swp *.swo +.idea/ # RVM .rvmrc diff --git a/.mdl_style.rb b/.mdl_style.rb new file mode 100644 index 00000000..60636ece --- /dev/null +++ b/.mdl_style.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +rule 'MD013', line_length: 120 +rule 'MD029', style: 'ordered' diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 00000000..1f82ca2c --- /dev/null +++ b/.mdlrc @@ -0,0 +1 @@ +style '.mdl_style.rb' diff --git a/.rubocop.disabled.yml b/.rubocop.disabled.yml index 7235d2a9..202a9970 100644 --- a/.rubocop.disabled.yml +++ b/.rubocop.disabled.yml @@ -38,16 +38,6 @@ Layout/MultilineAssignmentLayout: # Description: 'Checks unsafe usage of number conversion methods.' # Enabled: false -# By default, the rails cops are not run. Override in project or home -# directory .rubocop.yml files, or by giving the -R/--rails option. -Rails: - Enabled: false - -Rails/SaveBang: - Description: 'Identifies possible cases where Active Record save! or related should be used.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#save-bang' - Enabled: false - Style/AutoResourceCleanup: Description: 'Suggests the usage of an auto resource cleanup version of a method (if available).' Enabled: false diff --git a/.rubocop.enabled.yml b/.rubocop.enabled.yml index 44bb9fef..5310d679 100644 --- a/.rubocop.enabled.yml +++ b/.rubocop.enabled.yml @@ -1,4 +1,8 @@ # These are all the cops that are enabled in the default configuration. +require: + - rubocop-performance + - rubocop-rake + - rubocop-rspec #################### Bundler ############################### @@ -58,20 +62,20 @@ Layout/AccessModifierIndentation: StyleGuide: '#indent-public-private-protected' Enabled: true -Layout/AlignArray: +Layout/ArrayAlignment: Description: >- Align the elements of an array literal if they span more than one line. StyleGuide: '#align-multiline-arrays' Enabled: true -Layout/AlignHash: +Layout/HashAlignment: Description: >- Align the elements of a hash literal if they span more than one line. Enabled: true -Layout/AlignParameters: +Layout/ParameterAlignment: Description: >- Align the parameters of a method call if they span more than one line. @@ -175,23 +179,23 @@ Layout/FirstParameterIndentation: Description: 'Checks the indentation of the first parameter in a method call.' Enabled: true -Layout/IndentArray: +Layout/FirstArrayElementIndentation: Description: >- Checks the indentation of the first element in an array literal. Enabled: true -Layout/IndentAssignment: +Layout/AssignmentIndentation: Description: >- Checks the indentation of the first line of the right-hand-side of a multi-line assignment. Enabled: true -Layout/IndentHash: +Layout/FirstHashElementIndentation: Description: 'Checks the indentation of the first key in a hash literal.' Enabled: true -Layout/IndentHeredoc: +Layout/HeredocIndentation: Description: 'This cops checks the indentation of the here document bodies.' StyleGuide: '#squiggly-heredocs' Enabled: true @@ -385,12 +389,12 @@ Layout/SpaceInsideStringInterpolation: StyleGuide: '#string-interpolation' Enabled: true -Layout/Tab: +Layout/IndentationStyle: Description: 'No hard tabs.' StyleGuide: '#spaces-indentation' Enabled: true -Layout/TrailingBlankLines: +Layout/TrailingEmptyLines: Description: 'Checks trailing blank lines and final newline.' StyleGuide: '#newline-eof' Enabled: true @@ -432,7 +436,7 @@ Lint/AssignmentInCondition: # Description: '`BigDecimal.new()` is deprecated. Use `BigDecimal()` instead.' # Enabled: true -Lint/BlockAlignment: +Layout/BlockAlignment: Description: 'Align block ends correctly.' Enabled: true @@ -444,7 +448,7 @@ Lint/CircularArgumentReference: Description: "Default values in optional keyword arguments and optional ordinal arguments should not refer back to the name of the argument." Enabled: true -Lint/ConditionPosition: +Layout/ConditionPosition: Description: >- Checks for condition placed in a confusing position relative to the keyword. @@ -455,7 +459,7 @@ Lint/Debugger: Description: 'Check for debugger calls.' Enabled: true -Lint/DefEndAlignment: +Layout/DefEndAlignment: Description: 'Align ends corresponding to defs correctly.' Enabled: true @@ -471,7 +475,7 @@ Lint/DuplicateMethods: Description: 'Check for duplicate method definitions.' Enabled: true -Lint/DuplicatedKey: +Lint/DuplicateHashKey: Description: 'Check for duplicate keys in hash literals.' Enabled: true @@ -500,14 +504,10 @@ Lint/EmptyWhen: Description: 'Checks for `when` branches with empty bodies.' Enabled: true -Lint/EndAlignment: +Layout/EndAlignment: Description: 'Align ends correctly.' Enabled: true -Lint/EndInMethod: - Description: 'END blocks should not be placed inside method definitions.' - Enabled: true - Lint/EnsureReturn: Description: 'Do not use return in an ensure block.' StyleGuide: '#no-return-ensure' @@ -523,7 +523,7 @@ Lint/FormatParameterMismatch: Description: 'The number of parameters to format/sprint must match the fields.' Enabled: true -Lint/HandleExceptions: +Lint/SuppressedException: Description: "Don't suppress exception." StyleGuide: '#dont-hide-exceptions' Enabled: true @@ -567,7 +567,7 @@ Lint/MissingCopEnableDirective: Description: 'Checks for a `# rubocop:enable` after `# rubocop:disable`' Enabled: true -Lint/MultipleCompare: +Lint/MultipleComparison: Description: "Use `&&` operator to compare multiple value." Enabled: true @@ -674,15 +674,11 @@ Lint/ShadowingOuterLocalVariable: for block arguments or block local variables. Enabled: true -Lint/StringConversionInInterpolation: +Lint/RedundantStringCoercion: Description: 'Checks for Object#to_s usage in string interpolation.' StyleGuide: '#no-to-s' Enabled: true -Lint/Syntax: - Description: 'Checks syntax error' - Enabled: true - Lint/UnderscorePrefixedVariableName: Description: 'Do not use prefix `_` for a variable that is used.' Enabled: true @@ -703,11 +699,11 @@ Lint/UnifiedInteger: # # Enabled: true -Lint/UnneededRequireStatement: +Lint/RedundantRequireStatement: Description: 'Checks for unnecessary `require` statement.' Enabled: true -Lint/UnneededSplatExpansion: +Lint/RedundantSplatExpansion: Description: 'Checks for splat unnecessarily being called on literals' Enabled: true @@ -750,7 +746,7 @@ Lint/UselessAssignment: StyleGuide: '#underscore-unused-vars' Enabled: true -Lint/UselessComparison: +Lint/BinaryOperatorWithIdenticalOperands: Description: 'Checks for comparison of something with itself.' Enabled: true @@ -794,7 +790,7 @@ Metrics/CyclomaticComplexity: of test cases needed to validate a method. Enabled: true -Metrics/LineLength: +Layout/LineLength: Description: 'Limit lines to 80 characters.' StyleGuide: '#80-character-limits' Enabled: true @@ -923,8 +919,8 @@ Performance/Count: # This cop has known compatibility issues with `ActiveRecord` and other # frameworks. ActiveRecord's `count` ignores the block that is passed to it. # For more information, see the documentation in the cop itself. - # If you understand the known risk, you can disable `SafeMode`. - SafeMode: true + # If you understand the known risk, you can disable `SafeAutoCorrect`. + SafeAutoCorrect: true Enabled: true Performance/Detect: @@ -936,7 +932,7 @@ Performance/Detect: # frameworks. `ActiveRecord` does not implement a `detect` method and `find` # has its own meaning. Correcting `ActiveRecord` methods with this cop # should be considered unsafe. - SafeMode: true + SafeAutoCorrect: true Enabled: true Performance/DoubleStartEndWith: @@ -971,7 +967,7 @@ Performance/FlatMap: # This can be dangerous since `flat_map` will only flatten 1 level, and # `flatten` without any parameters can flatten multiple levels. -Performance/HashEachMethods: +Style/HashEachMethods: Description: >- Use `Hash#each_key` and `Hash#each_value` instead of `Hash#keys.each` and `Hash#values.each`. @@ -979,10 +975,6 @@ Performance/HashEachMethods: Enabled: true AutoCorrect: false -Performance/LstripRstrip: - Description: 'Use `strip` instead of `lstrip.rstrip`.' - Enabled: true - Performance/RangeInclude: Description: 'Use `Range#cover?` instead of `Range#include?`.' Reference: 'https://github.com/JuanitoFatas/fast-ruby#cover-vs-include-code' @@ -1004,7 +996,7 @@ Performance/RedundantMerge: Reference: 'https://github.com/JuanitoFatas/fast-ruby#hashmerge-vs-hash-code' Enabled: true -Performance/RedundantSortBy: +Style/RedundantSortBy: Description: 'Use `sort` instead of `sort_by { |x| x }`.' Enabled: true @@ -1019,7 +1011,7 @@ Performance/ReverseEach: Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code' Enabled: true -Performance/Sample: +Style/Sample: Description: >- Use `sample` instead of `shuffle.first`, `shuffle.last`, and `shuffle[Integer]`. @@ -1063,205 +1055,6 @@ Performance/UriDefaultParser: Description: 'Use `URI::DEFAULT_PARSER` instead of `URI::Parser.new`.' Enabled: true -#################### Rails ################################# - -Rails/ActionFilter: - Description: 'Enforces consistent use of action filter methods.' - Enabled: true - -Rails/ActiveSupportAliases: - Description: >- - Avoid ActiveSupport aliases of standard ruby methods: - `String#starts_with?`, `String#ends_with?`, - `Array#append`, `Array#prepend`. - Enabled: true - -Rails/ApplicationJob: - Description: 'Check that jobs subclass ApplicationJob.' - Enabled: true - -Rails/ApplicationRecord: - Description: 'Check that models subclass ApplicationRecord.' - Enabled: true - -Rails/Blank: - Description: 'Enforce using `blank?` and `present?`.' - Enabled: true - # Convert checks for `nil` or `empty?` to `blank?` - NilOrEmpty: true - # Convert usages of not `present?` to `blank?` - NotPresent: true - # Convert usages of `unless` `present?` to `if` `blank?` - UnlessPresent: true - -Rails/CreateTableWithTimestamps: - Description: >- - Checks the migration for which timestamps are not included - when creating a new table. - Enabled: true - -Rails/Date: - Description: >- - Checks the correct usage of date aware methods, - such as Date.today, Date.current etc. - Enabled: true - -Rails/Delegate: - Description: 'Prefer delegate method for delegations.' - Enabled: true - -Rails/DelegateAllowBlank: - Description: 'Do not use allow_blank as an option to delegate.' - Enabled: true - -Rails/DynamicFindBy: - Description: 'Use `find_by` instead of dynamic `find_by_*`.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#find_by' - Enabled: true - -Rails/EnumUniqueness: - Description: 'Avoid duplicate integers in hash-syntax `enum` declaration.' - Enabled: true - -Rails/EnvironmentComparison: - Description: "Favor `Rails.env.production?` over `Rails.env == 'production'`" - Enabled: true - -Rails/Exit: - Description: >- - Favor `fail`, `break`, `return`, etc. over `exit` in - application or library code outside of Rake files to avoid - exits during unit testing or running in production. - Enabled: true - -Rails/FilePath: - Description: 'Use `Rails.root.join` for file path joining.' - Enabled: true - -Rails/FindBy: - Description: 'Prefer find_by over where.first.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#find_by' - Enabled: true - -Rails/FindEach: - Description: 'Prefer all.find_each over all.find.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#find-each' - Enabled: true - -Rails/HasAndBelongsToMany: - Description: 'Prefer has_many :through to has_and_belongs_to_many.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has-many-through' - Enabled: true - -Rails/HasManyOrHasOneDependent: - Description: 'Define the dependent option to the has_many and has_one associations.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has_many-has_one-dependent-option' - Enabled: true - -Rails/HttpPositionalArguments: - Description: 'Use keyword arguments instead of positional arguments in http method calls.' - Enabled: true - Include: - - 'spec/**/*' - - 'test/**/*' - -Rails/InverseOf: - Description: 'Checks for associations where the inverse cannot be determined automatically.' - Enabled: true - -Rails/LexicallyScopedActionFilter: - Description: "Checks that methods specified in the filter's `only` or `except` options are explicitly defined in the controller." - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#lexically-scoped-action-filter' - Enabled: true - -Rails/NotNullColumn: - Description: 'Do not add a NOT NULL column without a default value' - Enabled: true - -Rails/Output: - Description: 'Checks for calls to puts, print, etc.' - Enabled: true - -Rails/OutputSafety: - Description: 'The use of `html_safe` or `raw` may be a security risk.' - Enabled: true - -Rails/PluralizationGrammar: - Description: 'Checks for incorrect grammar when using methods like `3.day.ago`.' - Enabled: true - -Rails/Presence: - Description: 'Checks code that can be written more easily using `Object#presence` defined by Active Support.' - Enabled: true - -Rails/Present: - Description: 'Enforce using `blank?` and `present?`.' - Enabled: true - NotNilAndNotEmpty: true - # Convert checks for not `nil` and not `empty?` to `present?` - NotBlank: true - # Convert usages of not `blank?` to `present?` - UnlessBlank: true - # Convert usages of `unless` `blank?` to `if` `present?` - -Rails/ReadWriteAttribute: - Description: >- - Checks for read_attribute(:attr) and - write_attribute(:attr, val). - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#read-attribute' - Enabled: true - -Rails/RedundantReceiverInWithOptions: - Description: 'Checks for redundant receiver in `with_options`.' - Enabled: true - -Rails/RelativeDateConstant: - Description: 'Do not assign relative date to constants.' - Enabled: true - -Rails/RequestReferer: - Description: 'Use consistent syntax for request.referer.' - Enabled: true - -Rails/ReversibleMigration: - Description: 'Checks whether the change method of the migration file is reversible.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#reversible-migration' - Reference: 'http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html' - Enabled: true - -Rails/SafeNavigation: - Description: "Use Ruby's safe navigation operator (`&.`) instead of `try!`" - Enabled: true - -Rails/ScopeArgs: - Description: 'Checks the arguments of ActiveRecord scopes.' - Enabled: true - -Rails/SkipsModelValidations: - Description: >- - Use methods that skips model validations with caution. - See reference for more information. - Reference: 'http://guides.rubyonrails.org/active_record_validations.html#skipping-validations' - Enabled: true - -Rails/TimeZone: - Description: 'Checks the correct usage of time zone aware methods.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' - Reference: 'http://danilenko.org/2012/7/6/rails_timezones' - Enabled: true - -Rails/UniqBeforePluck: - Description: 'Prefer the use of uniq or distinct before pluck.' - Enabled: true - -Rails/UnknownEnv: - Description: 'Use correct environment name.' - Enabled: true - -Rails/Validation: - Description: 'Use validates :attribute, hash of validations.' - Enabled: true - #################### Security ############################## Security/Eval: @@ -1346,10 +1139,6 @@ Style/BlockDelimiters: StyleGuide: '#single-line-blocks' Enabled: true -Style/BracesAroundHashParameters: - Description: 'Enforce braces style around hash parameters.' - Enabled: true - Style/CaseEquality: Description: 'Avoid explicit use of the case equality operator(===).' StyleGuide: '#no-case-equality' @@ -1497,7 +1286,7 @@ Style/EvenOdd: # Description: "Use `expand_path(__dir__)` instead of `expand_path('..', __FILE__)`." # Enabled: true -Style/FlipFlop: +Lint/FlipFlop: Description: 'Checks for flip flops' StyleGuide: '#no-flip-flops' Enabled: true @@ -1607,7 +1396,12 @@ Style/MethodDefParentheses: StyleGuide: '#method-parens' Enabled: true -Style/MethodMissing: +Lint/MissingSuper: + Description: 'Avoid using `method_missing`.' + StyleGuide: '#no-method-missing' + Enabled: true + +Style/MissingRespondToMissing: Description: 'Avoid using `method_missing`.' StyleGuide: '#no-method-missing' Enabled: true @@ -1975,15 +1769,15 @@ Style/UnlessElse: StyleGuide: '#no-else-with-unless' Enabled: true -Style/UnneededCapitalW: +Style/RedundantCapitalW: Description: 'Checks for %W when interpolation is not needed.' Enabled: true -Style/UnneededInterpolation: +Style/RedundantInterpolation: Description: 'Checks for strings that are just an interpolated expression.' Enabled: true -Style/UnneededPercentQ: +Style/RedundantPercentQ: Description: 'Checks for %q/%Q when single quotes or double quotes would do.' StyleGuide: '#percent-q' Enabled: true diff --git a/.rubocop.yml b/.rubocop.yml index 5190c275..293c7a06 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,7 +17,8 @@ AllCops: - 'pkg/**/*' - 'reports/**/*' - 'tmp/**/*' - TargetRubyVersion: 2.5.0 + - 'vendor/**/*' + TargetRubyVersion: 2.7.0 #################### Layout ########################### @@ -25,8 +26,42 @@ AllCops: Layout/AccessModifierIndentation: EnforcedStyle: indent +Layout/ArgumentAlignment: + EnforcedStyle: with_fixed_indentation + +# checks whether the end keywords are aligned properly for `do` `end` blocks. +Layout/BlockAlignment: + # The value `start_of_block` means that the `end` should be aligned with line + # where the `do` keyword appears. + # The value `start_of_line` means it should be aligned with the whole + # expression's starting line. + # The value `either` means both are allowed. + EnforcedStyleAlignWith: start_of_line + +Layout/DefEndAlignment: + # The value `def` means that `end` should be aligned with the def keyword. + # The value `start_of_line` means that `end` should be aligned with method + # calls like `private`, `public`, etc, if present in front of the `def` + # keyword on the same line. + EnforcedStyleAlignWith: start_of_line + # AutoCorrect: false + Severity: warning + +# Align ends correctly. +Layout/EndAlignment: + # The value `keyword` means that `end` should be aligned with the matching + # keyword (`if`, `while`, etc.). + # The value `variable` means that in assignments, `end` should be aligned + # with the start of the variable on the left hand side of `=`. In all other + # situations, `end` should still be aligned with the keyword. + # The value `start_of_line` means that `end` should be aligned with the start + # of the line which the matching keyword appears on. + EnforcedStyleAlignWith: start_of_line + # AutoCorrect: false + Severity: warning + # Align the elements of a hash literal if they span more than one line. -Layout/AlignHash: +Layout/HashAlignment: # Alignment of entries using colon as separator. Valid values are: # # key - left alignment of keys @@ -72,7 +107,24 @@ Layout/AlignHash: # b: 2) EnforcedLastArgumentHashStyle: ignore_implicit -Layout/AlignParameters: +Layout/LineLength: + Max: 120 + # To make it possible to copy or click on URIs in the code, we allow lines + # containing a URI to be longer than Max. + AllowHeredoc: true + AllowURI: true + URISchemes: + - http + - https + # The IgnoreCopDirectives option causes the LineLength rule to ignore cop + # directives like '# rubocop: enable ...' when calculating a line's length. + IgnoreCopDirectives: false + # The IgnoredPatterns option is a list of !ruby/regexp and/or string + # elements. Strings will be converted to Regexp objects. A line that matches + # any regular expression listed in this option will be ignored by LineLength. + IgnoredPatterns: [] + +Layout/ParameterAlignment: # Alignment of parameters in multi-line method calls. # # The `with_first_parameter` style aligns the following lines along the same @@ -166,7 +218,7 @@ Layout/FirstParameterIndentation: # # Same as `special_for_inner_method_call` except that the special rule only # # applies if the outer method call encloses its arguments in parentheses. # - special_for_inner_method_call_in_parentheses - EnforcedStyle: special_for_inner_method_call_in_parentheses + EnforcedStyle: consistent Layout/IndentationConsistency: # The difference between `rails` and `normal` is that the `rails` style @@ -183,7 +235,7 @@ Layout/IndentationWidth: IgnoredPatterns: [] # Checks the indentation of the first element in an array literal. -Layout/IndentArray: +Layout/FirstArrayElementIndentation: # The value `special_inside_parentheses` means that array literals with # brackets that have their opening bracket on the same line as a surrounding # opening round parenthesis, shall have their first element indented relative @@ -205,13 +257,13 @@ Layout/IndentArray: IndentationWidth: ~ # Checks the indentation of assignment RHS, when on a different line from LHS -Layout/IndentAssignment: +Layout/AssignmentIndentation: # By default, the indentation width from `Layout/IndentationWidth` is used # But it can be overridden by setting this parameter IndentationWidth: ~ # Checks the indentation of the first key in a hash literal. -Layout/IndentHash: +Layout/FirstHashElementIndentation: # The value `special_inside_parentheses` means that hash literals with braces # that have their opening brace on the same line as a surrounding opening # round parenthesis, shall have their first key indented relative to the @@ -232,7 +284,7 @@ Layout/IndentHash: # But it can be overridden by setting this parameter IndentationWidth: ~ -Layout/IndentHeredoc: +Layout/HeredocIndentation: EnforcedStyle: auto_detection SupportedStyles: - auto_detection @@ -427,14 +479,14 @@ Layout/ClassStructure: - protected_methods - private_methods -Layout/Tab: +Layout/IndentationStyle: # By default, the indentation width from Layout/IndentationWidth is used # But it can be overridden by setting this parameter # It is used during auto-correction to determine how many spaces should # replace each tab. IndentationWidth: ~ -Layout/TrailingBlankLines: +Layout/TrailingEmptyLines: EnforcedStyle: final_newline SupportedStyles: - final_newline @@ -504,7 +556,7 @@ Naming/FileName: - XSS Naming/HeredocDelimiterNaming: - Blacklist: + ForbiddenDelimiters: - END - !ruby/regexp '/EO[A-Z]{1}/' @@ -527,13 +579,13 @@ Naming/PredicateName: - has_ - have_ # Predicate name prefixes that should be removed. - NamePrefixBlacklist: + ForbiddenPrefixes: - is_ - has_ - have_ # Predicate names which, despite having a blacklisted prefix, or no `?`, # should still be accepted - NameWhitelist: + AllowedMethods: - is_a? # Method definition macros for dynamically generated methods. MethodDefinitionMacros: @@ -548,20 +600,16 @@ Naming/PredicateName: # # Parameter names may be equal to or greater than this value # MinNameLength: 1 # AllowNamesEndingInNumbers: true -# # Whitelisted names that will not register an offense # AllowedNames: [] -# # Blacklisted names that will register an offense # ForbiddenNames: [] # # Naming/UncommunicativeMethodParamName: # # Parameter names may be equal to or greater than this value # MinNameLength: 3 # AllowNamesEndingInNumbers: true -# # Whitelisted names that will not register an offense # AllowedNames: # - io # - id -# # Blacklisted names that will register an offense # ForbiddenNames: [] Naming/VariableName: @@ -676,20 +724,6 @@ Style/BlockDelimiters: - proc - it -Style/BracesAroundHashParameters: - EnforcedStyle: context_dependent - SupportedStyles: - # The `braces` style enforces braces around all method parameters that are - # hashes. - - braces - # The `no_braces` style checks that the last parameter doesn't have braces - # around it. - - no_braces - # The `context_dependent` style checks that the last parameter doesn't have - # braces around it, but requires braces if the second to last parameter is - # also a hash literal. - - context_dependent - Style/ClassAndModuleChildren: # Checks the style of children definitions at classes and modules. # @@ -817,6 +851,10 @@ Style/EmptyMethod: - compact - expanded +# Disable explicit block argument as it has a significant effect on performance +Style/ExplicitBlockArgument: + Enabled: false + # Checks use of for or each in multiline loops. Style/For: EnforcedStyle: each @@ -844,19 +882,7 @@ Style/FormatStringToken: - unannotated Style/FrozenStringLiteralComment: - EnforcedStyle: when_needed - SupportedStyles: - # `when_needed` will add the frozen string literal comment to files - # only when the `TargetRubyVersion` is set to 2.3+. - - when_needed - # `always` will always add the frozen string literal comment to a file - # regardless of the Ruby version or if `freeze` or `<<` are called on a - # string literal. If you run code against multiple versions of Ruby, it is - # possible that this will create errors in Ruby 2.3.0+. - - always - # `never` will enforce that the frozen string literal comment does not - # exist in a file. - - never + EnforcedStyle: always_true # Built-in global variables are allowed by default. Style/GlobalVars: @@ -1188,7 +1214,18 @@ Style/TrailingCommaInArguments: # for all parenthesized method calls with arguments. EnforcedStyleForMultiline: comma -Style/TrailingCommaInLiteral: +Style/TrailingCommaInArrayLiteral: +# # SupportedStylesForMultiline: +# # - comma +# # - consistent_comma +# # - no_comma +# # If `comma`, the cop requires a comma after the last item in an array, +# # but only when each item is on its own line. +# # If `consistent_comma`, the cop requires a comma after the last item of all +# # non-empty array literals. + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: # # SupportedStylesForMultiline: # # - comma # # - consistent_comma @@ -1244,7 +1281,7 @@ Style/TrivialAccessors: # Commonly used in DSLs AllowDSLWriters: false IgnoreClassMethods: false - Whitelist: + AllowedMethods: - to_ary - to_a - to_c @@ -1281,19 +1318,14 @@ Style/WordArray: WordRegex: !ruby/regexp '/\A[\p{Word}\n\t]+\z/' Style/YodaCondition: - EnforcedStyle: all_comparison_operators - SupportedStyles: - # check all comparison operators - - all_comparison_operators - # check only equality operators: `!=` and `==` - - equality_operators_only + EnforcedStyle: require_for_all_comparison_operators #################### Metrics ############################### Metrics/AbcSize: # The ABC size is a calculated magnitude, so this number can be an Integer or # a Float. - Max: 15 + Max: 20 Exclude: - 'tasks/*.rb' @@ -1304,6 +1336,7 @@ Metrics/BlockLength: Exclude: - 'test/**/*' - 'spec/**/*' + - 'tasks/ips.rb' Metrics/BlockNesting: CountBlocks: false @@ -1320,23 +1353,6 @@ Metrics/ClassLength: Metrics/CyclomaticComplexity: Max: 6 -Metrics/LineLength: - Max: 80 - # To make it possible to copy or click on URIs in the code, we allow lines - # containing a URI to be longer than Max. - AllowHeredoc: true - AllowURI: true - URISchemes: - - http - - https - # The IgnoreCopDirectives option causes the LineLength rule to ignore cop - # directives like '# rubocop: enable ...' when calculating a line's length. - IgnoreCopDirectives: false - # The IgnoredPatterns option is a list of !ruby/regexp and/or string - # elements. Strings will be converted to Regexp objects. A line that matches - # any regular expression listed in this option will be ignored by LineLength. - IgnoredPatterns: [] - Metrics/MethodLength: CountComments: false # count full line comments? Max: 20 @@ -1347,6 +1363,7 @@ Metrics/ModuleLength: Metrics/ParameterLists: Max: 5 + MaxOptionalParameters: 5 CountKeywordArgs: true Metrics/PerceivedComplexity: @@ -1358,38 +1375,7 @@ Metrics/PerceivedComplexity: Lint/AssignmentInCondition: AllowSafeAssignment: true -# checks whether the end keywords are aligned properly for `do` `end` blocks. -Lint/BlockAlignment: - # The value `start_of_block` means that the `end` should be aligned with line - # where the `do` keyword appears. - # The value `start_of_line` means it should be aligned with the whole - # expression's starting line. - # The value `either` means both are allowed. - EnforcedStyleAlignWith: start_of_line - -Lint/DefEndAlignment: - # The value `def` means that `end` should be aligned with the def keyword. - # The value `start_of_line` means that `end` should be aligned with method - # calls like `private`, `public`, etc, if present in front of the `def` - # keyword on the same line. - EnforcedStyleAlignWith: start_of_line - # AutoCorrect: false - Severity: warning - -# Align ends correctly. -Lint/EndAlignment: - # The value `keyword` means that `end` should be aligned with the matching - # keyword (`if`, `while`, etc.). - # The value `variable` means that in assignments, `end` should be aligned - # with the start of the variable on the left hand side of `=`. In all other - # situations, `end` should still be aligned with the keyword. - # The value `start_of_line` means that `end` should be aligned with the start - # of the line which the matching keyword appears on. - EnforcedStyleAlignWith: start_of_line - # AutoCorrect: false - Severity: warning - -Lint/HandleExceptions: +Lint/SuppressedException: Exclude: - 'spec/**/*' @@ -1411,7 +1397,7 @@ Lint/MissingCopEnableDirective: MaximumRangeSize: .inf Lint/SafeNavigationChain: - Whitelist: + AllowedMethods: - present? - blank? - presence @@ -1434,6 +1420,14 @@ Lint/UnusedMethodArgument: # Lint/Void: # CheckForMethodsWithNoSideEffects: false +#################### RSpec ################################# + +RSpec/MultipleMemoizedHelpers: + Max: 10 + +RSpec/NestedGroups: + Max: 5 + #################### Performance ########################### Performance/DoubleStartEndWith: @@ -1445,152 +1439,6 @@ Performance/RedundantMerge: # Max number of key-value pairs to consider an offense MaxKeyValuePairs: 2 -#################### Rails ################################# - -Rails/ActionFilter: - EnforcedStyle: action - SupportedStyles: - - action - - filter - Include: - - app/controllers/**/*.rb - -Rails/CreateTableWithTimestamps: - Include: - - db/migrate/*.rb - -Rails/Date: - # The value `strict` disallows usage of `Date.today`, `Date.current`, - # `Date#to_time` etc. - # The value `flexible` allows usage of `Date.current`, `Date.yesterday`, etc - # (but not `Date.today`) which are overridden by ActiveSupport to handle current - # time zone. - EnforcedStyle: flexible - SupportedStyles: - - strict - - flexible - -Rails/Delegate: - # When set to true, using the target object as a prefix of the - # method name without using the `delegate` method will be a - # violation. When set to false, this case is legal. - EnforceForPrefixed: true - -Rails/DynamicFindBy: - Whitelist: - - find_by_sql - -Rails/EnumUniqueness: - Include: - - app/models/**/*.rb - -Rails/Exit: - Include: - - app/**/*.rb - - config/**/*.rb - - lib/**/*.rb - Exclude: - - lib/**/*.rake - -Rails/FindBy: - Include: - - app/models/**/*.rb - -Rails/FindEach: - Include: - - app/models/**/*.rb - -Rails/HasAndBelongsToMany: - Include: - - app/models/**/*.rb - -Rails/HasManyOrHasOneDependent: - Include: - - app/models/**/*.rb - -Rails/InverseOf: - Include: - - app/models/**/*.rb - -Rails/LexicallyScopedActionFilter: - Include: - - app/controllers/**/*.rb - -Rails/NotNullColumn: - Include: - - db/migrate/*.rb - -Rails/Output: - Include: - - app/**/*.rb - - config/**/*.rb - - db/**/*.rb - - lib/**/*.rb - -Rails/ReadWriteAttribute: - Include: - - app/models/**/*.rb - -Rails/RequestReferer: - EnforcedStyle: referer - SupportedStyles: - - referer - - referrer - -Rails/ReversibleMigration: - Include: - - db/migrate/*.rb - -Rails/SafeNavigation: - # This will convert usages of `try` to use safe navigation as well as `try!`. - # `try` and `try!` work slightly differently. `try!` and safe navigation will - # both raise a `NoMethodError` if the receiver of the method call does not - # implement the intended method. `try` will not raise an exception for this. - ConvertTry: false - -Rails/ScopeArgs: - Include: - - app/models/**/*.rb - -Rails/TimeZone: - # The value `strict` means that `Time` should be used with `zone`. - # The value `flexible` allows usage of `in_time_zone` instead of `zone`. - EnforcedStyle: flexible - SupportedStyles: - - strict - - flexible - -Rails/UniqBeforePluck: - EnforcedStyle: conservative - SupportedStyles: - - conservative - - aggressive - AutoCorrect: false - -Rails/UnknownEnv: - Environments: - - development - - test - - production - -Rails/SkipsModelValidations: - Blacklist: - - decrement! - - decrement_counter - - increment! - - increment_counter - - toggle! - - touch - - update_all - - update_attribute - - update_column - - update_columns - - update_counters - -Rails/Validation: - Include: - - app/models/**/*.rb - Bundler/OrderedGems: TreatCommentsAsGroupSeparators: true diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml new file mode 100644 index 00000000..d36f9f62 --- /dev/null +++ b/.semaphore/semaphore.yml @@ -0,0 +1,32 @@ +version: v1.0 +name: Run specs in latest ruby +agent: + machine: + type: e1-standard-2 + os_image: ubuntu2004 +global_job_config: + prologue: + commands: + - checkout +blocks: + - name: 3.2.2 + dependencies: [] + task: + prologue: + commands: + - sem-version ruby 3.2.2 + - bundle install + jobs: + - name: bundle exec rspec + commands: + - bundle exec rspec --format RspecJunitFormatter --out report.xml + epilogue: + always: + commands: + - '[[ -f report.xml ]] && test-results publish report.xml' +after_pipeline: + task: + jobs: + - name: test report + commands: + - test-results gen-pipeline-report diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bea30e24..00000000 --- a/.travis.yml +++ /dev/null @@ -1,35 +0,0 @@ -env: - global: - - CC_TEST_REPORTER_ID=6f07a33d1bf4060910c8b97cb9bf97230bbf1fad75765fef98f3cca9f29cd6b0 -language: ruby -before_install: - - gem install bundler -install: - - bundle install --without local -rvm: - - 2.7.1 - - 2.7.0 - - 2.6.6 - - 2.6.5 - - 2.6.4 - - 2.6.3 - - 2.6.2 - - 2.6.1 - - 2.6.0 - - 2.5.8 - - 2.5.7 - - 2.5.6 - - 2.5.5 - - 2.5.4 - - 2.5.3 - - 2.5.2 - - 2.5.1 - - 2.5.0 -before_script: - - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - - chmod +x ./cc-test-reporter - - ./cc-test-reporter before-build -script: - - bundle exec rspec -after_script: - - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d3ea85..276f98fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,101 @@ -# Changelog - -## 2.1.2 [compare][compare_v2_1_1_and_master] +# CHANGELOG + +## 2.3.2 [compare][compare_v2_3_1_and_master] + +## 2.3.1 [compare][compare_v2_3_0_and_v2_3_1] + +- Fix `Rambling::Trie.load` docs in README by [@gonzedge][github_user_gonzedge] +- Destructure args hash before passing to performance rake task by [@gonzedge][github_user_gonzedge] +- Update copyright years by [@gonzedge][github_user_gonzedge] +- Attempt to clear gem version badge being cached by GitHub (?) by [@gonzedge][github_user_gonzedge] +- Migrate from TravisCI to SemaphoreCI by [@gonzedge][github_user_gonzedge] +- Be more lax with file size tests ([#26][github_pull_26]) by [@gonzedge][github_user_gonzedge] +- Add missing `compress!` in container spec ([#25][github_pull_25]) by [@gonzedge][github_user_gonzedge] + - To actually test the non-matching tree shared example for compressed tries. +- Upgrade RuboCop and apply new rules ([#24][github_pull_24]) by [@gonzedge][github_user_gonzedge] + - Issues found in `lib/` and `tasks/` + 1. Explicitly disable `Style/ExplicitBlockArgument` due to resulting performance hits + 2. Add missing `super`s + 3. Yoda-style conditionals + 4. Bump target ruby required version to current min supported (`2.7.0`) + - Issues found in `spec/` + 1. Use `instance_double` instead of `double` + 2. Split tests into single-assertion specs when parameterization is possible; disable otherwise + 3. Use `let!` instead of instance variables + 4. Remove duplicate test groups + 5. Use `when` at the start of `context` block descriptions + 6. Use `described_class`, `subject` in specs where possible + 7. Single-line `before` where possible + - Other + 1. Explicitly add `rubocop-performance`, `rubocop-rake`, `rubocop-rspec` plugins + 2. Remove unused `Rails` rules + 3. Remove unused/deprecated `RSpec`/`Layout`/`Style`/`Lint` rules +- More thorough serializer tests ([#28][github_pull_28]) by [@gonzedge][github_user_gonzedge] + - Realized that, except for one integration test, we were not testing compressed tries being serialized so added some + more specific use cases. Also took the chance to expand the zip serializer spec to handle all formats. +- Update badges, CI and test/coverage reporting ([#29][github_pull_29]) by [@gonzedge][github_user_gonzedge] + - Correctly configure main task to publish test report, now being picked up by Semaphore + - On badges: + - Change `README.md` to have one badge per line instead of all in one giant line + - Add RubyGems downloads badge + - Add CodeClimate issues badge + - Update docs badge to point directly to [rubydoc.info/gems/rambling-trie][rubydoc] + - Update license badge to one from shields.io +- Use GitHub Actions for main branch and PR checks ([#30][github_pull_30], [#31][github_pull_31], [#32][github_pull_32]) + by [@gonzedge][github_user_gonzedge] + - Run `build` action with `lint` for `rubocop`, `spec` for `rspec`, `coverage` for `coveralls` + - Run `codeql` action + - Run `dependency-review` action only on pull requests +- Reduce semaphore config to latest ruby ([#33][github_pull_33]) by [@gonzedge][github_user_gonzedge] + - Now, we only care about top-level pass/fail for badge reporting. All other tests are run with GitHub Actions. +- Rename GitHub Actions and steps for better badges ([#34][github_pull_34]) by [@gonzedge][github_user_gonzedge] + - Plus reformat badges at top of `README.md`. +- Add CodeClimate coverage step to build GH action ([#35][github_pull_35]) by [@gonzedge][github_user_gonzedge] + - Do things differently for coveralls and code climate + - Use correct shared example for `words_within?` on compressed tries +- Ensure all serializer `#dump` methods return the size of the file ([#36][github_pull_36]) by [@gonzedge][github_user_gonzedge] + - Add test in `a serializer` shared examples + - Implement method for `Rambling::Trie::Serializers::Zip` +- Ensure `#each`/`#each_word` return `Enumerator`/`self` ([#37][github_pull_37]) by [@gonzedge][github_user_gonzedge] + - … depending on whether a block is given or not. +- Improve API documentation ([#38][github_pull_38]) by [@gonzedge][github_user_gonzedge] + - Add `Readers::Reader` and `Serializer::Serializer` base classes + - Make all readers/serializers extend from their corresponding base classes + - Better docs with `Reader`/`Serializer` and generics + - Fix all code blocks from backtick to `+` and add some more + - Add `@return [void]` where appropriate + - Add `@return [self]` where appropriate + - Fix `Nodes::Node` duplicate and broken references + - Fix some typos and add some missing periods +- Add explicit changelog and docs urls to `.gemspec` ([#39][github_pull_39]) by [@gonzedge][github_user_gonzedge] + - Update `CHANGELOG.md` with latest changes + +## 2.3.0 [compare][compare_v2_2_1_and_v2_3_0] + +- Don't use `YAML.safe_load`'s legacy API by [@KitaitiMakoto][github_user_kitaitimakoto] +- Add explicit support for Ruby 3.1.x by [@KitaitiMakoto][github_user_kitaitimakoto] +- Add block to `Coveralls.wear!` to prevent `SimpleCove.start` being called twice by [@KitaitiMakoto][github_user_kitaitimakoto] +- Add explicit support for Ruby 3.2.x by [@agate][github_user_agate] +- Make sure gem also supports all the sub version of 3.2 by [@agate][github_user_agate] + - Includes adding support for 2.7.{4,5,6,7}, 3.0.{2,3,4,5}, 3.1.{0,1,2,3} and 3.2.{0,1} +- Use new `coveralls_reborn` to support new ruby by [@agate][github_user_agate] +- Update `required_ruby_version` bounds to `>= 2.7, < 4` by [@gonzedge][github_user_gonzedge] +- Drop support for Ruby 2.5.x and 2.6.x by [@gonzedge][github_user_gonzedge] +- Add Ruby 2.7.8, 3.0.6, 3.1.4, 3.2.2 to supported versions by [@gonzedge][github_user_gonzedge] +- Update documentation links to min required ruby version by [@gonzedge][github_user_gonzedge] + +## 2.2.1 [compare][compare_v2_2_0_and_v2_2_1] + +- Add support for Ruby 3.0.x by [@as181920][github_user_as181920] + +## 2.2.0 [compare][compare_v2_1_1_and_v2_2_0] + +- Bump min version to 2.5 by [@gonzedge][github_user_gonzedge] +- Add Ruby 3 to required Ruby versions by [@KitaitiMakoto][github_user_kitaitimakoto] ## 2.1.1 [compare][compare_v2_1_0_and_v2_1_1] -- Change `slice!` to `shift` by [@shinjiikeda][github_user_shinjiikeda] +- Change `slice!` to `shift` (#16) by [@shinjiikeda][github_user_shinjiikeda] - Frozen string issue fix by [@godsent][github_user_godsent] - Drop Ruby 2.4.x; add 2.7 and updated 2.6.x/2.5.x support by [@gonzedge][github_user_gonzedge] - Be more flexible with file sizes for zip file test by [@gonzedge][github_user_gonzedge] @@ -82,7 +173,7 @@ Most of these help with the gem's overall performance. - Define delegate methods explicitly and remove dependency on `Forwardable` by [@gonzedge][github_user_gonzedge] - Reverse char array and use `#pop` instead of slice when adding a word by [@gonzedge][github_user_gonzedge] - Pull `#scan` up to `Node` by [@gonzedge][github_user_gonzedge] -- Slightly reduce memeory for `Properties` and `ProviderCollection` classes by [@gonzedge][github_user_gonzedge] +- Slightly reduce memory for `Properties` and `ProviderCollection` classes by [@gonzedge][github_user_gonzedge] - Use `#children_tree` instead of `#children` when possible by [@gonzedge][github_user_gonzedge] - Remove unnecessary assignment in `#letter=` by [@gonzedge][github_user_gonzedge] - Use `#each_value` instead of `#values`.`#each` in `Enumerable#each` by [@gonzedge][github_user_gonzedge] @@ -230,20 +321,15 @@ Most of these help with the gem's overall performance. - Add Ruby 2.4 to supported versions by [@gonzedge][github_user_gonzedge] - Drastically reduce size of gem by [@gonzedge][github_user_gonzedge] - - By excluding unnecessary `assets/` and `reports/` when building the gem. - **Size reduction**: from ~472KB to ~21KB. - + - By excluding unnecessary `assets/` and `reports/` when building the gem. + - **Size reduction**: from ~472KB to ~21KB. - Make root node accessible via container by [@gonzedge][github_user_gonzedge] - - So that anyone using rambling-trie can develop their custom algorithms - + - So that anyone using rambling-trie can develop their custom algorithms - Expose root node's `#to_a` method through `Container` by [@gonzedge][github_user_gonzedge] - Add own `Forwardable#delegate` because of [Ruby 2.4 performance degradation][ruby_bug_13111] by [@gonzedge][github_user_gonzedge] - - Was able to take Creation and Compression benchmarks (~8.8s and ~1.5s + - Was able to take Creation and Compression benchmarks (~8.8s and ~1.5s respectively) back down to the Ruby 2.3.3 levels by adding own definition of `Forwardable#delegate`. @@ -256,7 +342,7 @@ Most of these help with the gem's overall performance. [@gonzedge][github_user_gonzedge] - Add missing docs by [@gonzedge][github_user_gonzedge] - Improvements on TravisCI setup by [@gonzedge][github_user_gonzedge] -- Add codeclimate test coverage integration by +- Add CodeClimate test coverage integration by [@gonzedge][github_user_gonzedge] - Move rspec config from .rspec to spec_helper by [@gonzedge][github_user_gonzedge] @@ -322,16 +408,12 @@ Most of these help with the gem's overall performance. - `Rambling::Trie.create` now returns a `Container` instead of a `Root` by [@gonzedge][github_user_gonzedge] - - `Container` exposes these API entry points: - + - `Container` exposes these API entry points: - `#partial_word?` and its alias `#match?` - `#word?` and its alias `#include?` - `#add` and its alias `#<<` - yield the constructed `Container` on `#initialize` - - `Rambling::Trie::Node` and its subclasses no longer expose: - + - `Rambling::Trie::Node` and its subclasses no longer expose: - `#match?` - `#include?` - `#<<` @@ -638,7 +720,7 @@ Most of these help with the gem's overall performance. - Add guard to Gemfile by [@gonzedge][github_user_gonzedge] - Add simplecov for code coverage by [@gonzedge][github_user_gonzedge] - Refactor rambling-trie requires by [@gonzedge][github_user_gonzedge] -- Remov unnecessary internal `#trie_node` by [@gonzedge][github_user_gonzedge] +- Remove unnecessary internal `#trie_node` by [@gonzedge][github_user_gonzedge] - Refactor specs to "The RSpec Way" by [@gonzedge][github_user_gonzedge] - Add new benchmarking report info by [@gonzedge][github_user_gonzedge] - Update RubyDoc.info link and compression info by [@gonzedge][github_user_gonzedge] @@ -831,7 +913,11 @@ Most of these help with the gem's overall performance. [compare_v1_0_3_and_v2_0_0]: https://github.com/gonzedge/rambling-trie/compare/v1.0.3...v2.0.0 [compare_v2_0_0_and_v2_1_0]: https://github.com/gonzedge/rambling-trie/compare/v2.0.0...v2.1.0 [compare_v2_1_0_and_v2_1_1]: https://github.com/gonzedge/rambling-trie/compare/v2.1.0...v2.1.1 -[compare_v2_1_1_and_master]: https://github.com/gonzedge/rambling-trie/compare/v2.1.1...master +[compare_v2_1_1_and_v2_2_0]: https://github.com/gonzedge/rambling-trie/compare/v2.1.1...v2.2.0 +[compare_v2_2_0_and_v2_2_1]: https://github.com/gonzedge/rambling-trie/compare/v2.2.0...v2.2.1 +[compare_v2_2_1_and_v2_3_0]: https://github.com/gonzedge/rambling-trie/compare/v2.2.1...v2.3.0 +[compare_v2_3_0_and_v2_3_1]: https://github.com/gonzedge/rambling-trie/compare/v2.3.0...v2.3.1 +[compare_v2_3_1_and_master]: https://github.com/gonzedge/rambling-trie/compare/v2.3.1...master [design_patterns_null_object]: http://wiki.c2.com/?NullObject [github_commit_current_key_less_memory]: https://github.com/gonzedge/rambling-trie/commit/218fac218a77e70ba04a3672ff5abfddf6544f57 [github_commit_reduced_memory_footprint]: https://github.com/gonzedge/rambling-trie/commit/aa8c0262f888e88df6a2f1e1351d8f14b21e43c4 @@ -845,8 +931,36 @@ Most of these help with the gem's overall performance. [github_issue_09]: https://github.com/gonzedge/rambling-trie/issues/9 [github_issue_10]: https://github.com/gonzedge/rambling-trie/issues/10 [github_issue_11]: https://github.com/gonzedge/rambling-trie/issues/11 +[github_pull_16]: https://github.com/gonzedge/rambling-trie/pull/16 +[github_pull_17]: https://github.com/gonzedge/rambling-trie/pull/17 +[github_pull_18]: https://github.com/gonzedge/rambling-trie/pull/18 +[github_pull_19]: https://github.com/gonzedge/rambling-trie/pull/19 +[github_pull_20]: https://github.com/gonzedge/rambling-trie/pull/20 +[github_pull_21]: https://github.com/gonzedge/rambling-trie/pull/21 +[github_pull_22]: https://github.com/gonzedge/rambling-trie/pull/22 +[github_pull_23]: https://github.com/gonzedge/rambling-trie/pull/23 +[github_pull_24]: https://github.com/gonzedge/rambling-trie/pull/24 +[github_pull_25]: https://github.com/gonzedge/rambling-trie/pull/25 +[github_pull_26]: https://github.com/gonzedge/rambling-trie/pull/26 +[github_pull_27]: https://github.com/gonzedge/rambling-trie/pull/27 +[github_pull_28]: https://github.com/gonzedge/rambling-trie/pull/28 +[github_pull_29]: https://github.com/gonzedge/rambling-trie/pull/29 +[github_pull_30]: https://github.com/gonzedge/rambling-trie/pull/30 +[github_pull_31]: https://github.com/gonzedge/rambling-trie/pull/31 +[github_pull_32]: https://github.com/gonzedge/rambling-trie/pull/32 +[github_pull_33]: https://github.com/gonzedge/rambling-trie/pull/33 +[github_pull_34]: https://github.com/gonzedge/rambling-trie/pull/34 +[github_pull_35]: https://github.com/gonzedge/rambling-trie/pull/35 +[github_pull_36]: https://github.com/gonzedge/rambling-trie/pull/36 +[github_pull_37]: https://github.com/gonzedge/rambling-trie/pull/37 +[github_pull_38]: https://github.com/gonzedge/rambling-trie/pull/38 +[github_pull_39]: https://github.com/gonzedge/rambling-trie/pull/39 +[github_user_agate]: https://github.com/agate +[github_user_as181920]: https://github.com/as181920 [github_user_godsent]: https://github.com/godsent [github_user_gonzedge]: https://github.com/gonzedge +[github_user_kitaitimakoto]: https://github.com/KitaitiMakoto [github_user_lilibethdlc]: https://github.com/lilibethdlc [github_user_shinjiikeda]: https://github.com/shinjiikeda [ruby_bug_13111]: https://bugs.ruby-lang.org/issues/13111 +[rubydoc]: http://rubydoc.info/gems/rambling-trie diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb7a8aa8..cd209722 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,19 @@ -## Contributing to Rambling Trie +# Contributing to Rambling Trie -1. If you have found a bug or have a feature request, please [search through the issues][github_issues_all] to see if it has already been reported. If that's not the case, then [create a new one][github_issues_new] with a full description of what you have found or what you need. -1. If you have bug fix or a feature implementation in mind, then [fork Rambling Trie][github_fork] and create a branch with a descriptive name. -1. Get the gem up and running locally (tests are written in RSpec): +1. If you have found a bug or have a feature request, please [search through the issues][github_issues_all] to see if it + has already been reported. If that's not the case, then [create a new one][github_issues_new] with a full description + of what you have found or what you need. +2. If you have bug fix or a feature implementation in mind, then [fork Rambling Trie][github_fork] and create a branch + with a descriptive name. +3. Get the gem up and running locally (tests are written in RSpec): ```sh bundle install - rake + bundle exec rake ``` -1. Implement your bug fix or feature - ***make sure to add tests!*** -1. [Make a Pull Request][github_pull_request] +4. Implement your bug fix or feature - ***make sure to add tests!*** +5. [Make a Pull Request][github_pull_request] Before doing so: diff --git a/Gemfile b/Gemfile index bcb70db2..d0a2acf1 100644 --- a/Gemfile +++ b/Gemfile @@ -16,11 +16,17 @@ group :development do end group :test do - gem 'coveralls', '~>0.8.21', require: false + gem 'coveralls_reborn', '~> 0.27.0', require: false + gem 'rspec_junit_formatter' gem 'simplecov', require: false end group :local do + gem 'flog', require: false gem 'guard-rspec' + gem 'mdl', require: false gem 'rubocop', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rake', require: false + gem 'rubocop-rspec', require: false end diff --git a/LICENSE b/LICENSE index c66251ce..00e7e519 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2017 Edgar Gonzalez +Copyright (c) 2012-2023 Edgar Gonzalez MIT License diff --git a/README.md b/README.md index b9d5a04e..5acf6b46 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,19 @@ # Rambling Trie -[![Gem Version][badge_fury_badge]][badge_fury_link] [![Build Status][travis_ci_badge]][travis_ci_link] [![Code Climate][code_climate_badge]][code_climage_link] [![Coverage Status][coveralls_badge]][coveralls_link] [![Documentation Status][inch_ci_badge]][inch_ci_link] [![License][license_badge]][license_link] +[![Gem Version][badge_fury_badge]][badge_fury_link] +[![Downloads][downloads_badge]][downloads_link] +[![License][license_badge]][license_link] -The Rambling Trie is a Ruby implementation of the [trie data structure][trie_wiki], which includes compression abilities and is designed to be very fast to traverse. +[![Build Status][github_action_build_badge]][github_action_build_link] +[![Coverage Status][coveralls_badge]][coveralls_link] +[![Documentation Status][inch_ci_badge]][rubydoc] +[![CodeQL Status][github_action_codeql_badge]][github_action_codeql_link] + +[![Code Climate Grade][code_climate_grade_badge]][code_climate_link] +[![Code Climate Issue Count][code_climate_issues_badge]][code_climate_link] + +The Rambling Trie is a Ruby implementation of the [trie data structure][trie_wiki], which includes compression abilities +and is designed to be very fast to traverse. ## Installing the Rambling Trie @@ -10,7 +21,7 @@ The Rambling Trie is a Ruby implementation of the [trie data structure][trie_wik You will need: -* Ruby 2.5.0 or up +* Ruby 2.7.0 or up * RubyGems See [RVM][rvm], [rbenv][rbenv] or [chruby][chruby] for more information on how to manage Ruby versions. @@ -47,7 +58,8 @@ Rambling::Trie.create do |trie| end ``` -Additionally, you can provide the path to a file that contains all the words to be added to the trie, and it will read the file and create the complete structure for you, like this: +Additionally, you can provide the path to a file that contains all the words to be added to the trie, and it will read +the file and create the complete structure for you, like this: ``` ruby trie = Rambling::Trie.create '/path/to/file' @@ -64,7 +76,10 @@ the trie ``` -If you want to use a custom file format, you will need to provide a custom file reader that defines an `#each_word` method that yields each word contained in the file. Look at the [`PlainText` reader][rambling_trie_plain_text_reader] class for an example, and at the [Configuration section][rambling_trie_configuration] to see how to add your own custom file readers. +If you want to use a custom file format, you will need to provide a custom `Reader` that defines an `#each_word` method +that yields each word contained in the file. Look at the [`PlainText` reader][rambling_trie_plain_text_reader] class for +an example, and at the [Configuration section][rambling_trie_configuration] to see how to add your own custom file +readers. ### Operations @@ -88,7 +103,8 @@ trie.word? 'word' trie.include? 'word' ``` -If you wish to find if part of a word exists in the trie instance, you should call `#partial_word?` or its alias `#match?`: +If you wish to find if part of a word exists in the trie instance, you should call `#partial_word?` or its +alias `#match?`: ``` ruby trie.partial_word? 'partial_word' @@ -109,7 +125,8 @@ trie.words_within 'ifdxawesome45someword3' # => ['if', 'aw', 'awe', ...] trie.words_within 'tktktktk' # => [] ``` -Or, if you're just interested in knowing whether a given string contains any valid words or not, you can use `#words_within?`: +Or, if you're just interested in knowing whether a given string contains any valid words or not, you can +use `#words_within?`: ``` ruby trie.words_within? 'ifdxawesome45someword3' # => true @@ -118,13 +135,15 @@ trie.words_within? 'tktktktk' # => false ### Compression -By default, the Rambling Trie works as a standard trie. Starting from version 0.1.0, you can obtain a compressed trie from the standard one, by using the compression feature. Just call the `#compress!` method on the trie instance: +By default, the Rambling Trie works as a standard trie. Starting from version 0.1.0, you can obtain a compressed trie +from the standard one, by using the compression feature. Just call the `#compress!` method on the trie instance: ``` ruby trie.compress! ``` -This will reduce the size of the trie by using redundant node elimination (redundant nodes are the only-child non-terminal nodes). +This will reduce the size of the trie by using redundant node elimination (redundant nodes are the only-child +non-terminal nodes). > _**Note**: The `#compress!` method acts over the trie instance it belongs to > and replaces the root `Node`. Also, adding words after compression (with `#add` or @@ -145,7 +164,8 @@ compressed_trie.compressed? # => true ### Enumeration -Starting from version 0.4.2, you can use any `Enumerable` method over a trie instance, and it will iterate over each word contained in the trie. You can now do things like: +Starting from version 0.4.2, you can use any `Enumerable` method over a trie instance, and it will iterate over each +word contained in the trie. You can now do things like: ``` ruby trie.each { |word| puts word } @@ -156,7 +176,10 @@ trie.all? { |word| word.include? 'x' } ### Serialization -Starting from version 1.0.0, you can store a full trie instance on disk and retrieve/use it later on. Loading a trie from disk takes less time, less cpu and less memory than loading every word into the trie every time. This is particularly useful for production applications, when you have word lists that you know are going to be static, or that change with little frequency. +Starting from version 1.0.0, you can store a full trie instance on disk and retrieve/use it later on. Loading a trie +from disk takes less time, less cpu and less memory than loading every word into the trie every time. This is +particularly useful for production applications, when you have word lists that you know are going to be static, or that +change with little frequency. To store a trie on disk, you can use `.dump` like this: @@ -164,24 +187,26 @@ To store a trie on disk, you can use `.dump` like this: Rambling::Trie.dump trie, '/path/to/file' ``` -Then, when you need to use a trie next time, you don't have to create a new one with all the necessary words. Rather, you can retrieve a previously stored one with `.load` like this: +Then, when you need to use a trie next time, you don't have to create a new one with all the necessary words. Rather, +you can retrieve a previously stored one with `.load` like this: ``` ruby -trie = Rambling::Trie.load trie, '/path/to/file' +trie = Rambling::Trie.load '/path/to/file' ``` #### Supported formats Currently, these formats are supported to store tries on disk: -- Ruby's [binary (Marshal)][marshal] format -- [YAML][yaml] +* Ruby's [binary (Marshal)][marshal] format +* [YAML][yaml] > When dumping into or loading from disk, the format is determined > automatically based on the file extension, so `.yml` or `.yaml` files will be > handled through `YAML` and `.marshal` files through `Marshal`. -Optionally, you can use a `.zip` version of the supported formats. In order to do so, you'll have to install the [`rubyzip`][rubyzip] gem: +Optionally, you can use a `.zip` version of the supported formats. In order to do so, you'll have to install +the [`rubyzip`][rubyzip] gem: ``` bash gem install rubyzip @@ -197,7 +222,7 @@ Then, you can load contents form a `.zip` file like this: ``` ruby require 'zip' -trie = Rambling::Trie.load trie, '/path/to/file.zip' +trie = Rambling::Trie.load '/path/to/file.zip' ``` > For `.zip` files, the format is also determined automatically based on the @@ -236,57 +261,75 @@ end ### Further Documentation -You can find further API documentation on the autogenerated [rambling-trie gem RubyDoc.info page][rubydoc] or if you want edge documentation, you can go the [GitHub project RubyDoc.info page][rubydoc_github]. +You can find further API documentation on the autogenerated [rambling-trie gem RubyDoc.info page][rubydoc] or if you +want edge documentation, you can go the [GitHub project RubyDoc.info page][rubydoc_github]. ## Compatible Ruby and Rails versions The Rambling Trie has been tested with the following Ruby versions: +* 3.2.x +* 3.1.x +* 3.0.x * 2.7.x -* 2.6.x -* 2.5.x **No longer supported**: +* 2.6.x (EOL'ed) +* 2.5.x (EOL'ed) * 2.4.x (EOL'ed) * 2.3.x (EOL'ed) -* 2.2.x -* 2.1.x -* 2.0.x -* 1.9.x -* 1.8.x +* 2.2.x (EOL'ed) +* 2.1.x (EOL'ed) +* 2.0.x (EOL'ed) +* 1.9.x (EOL'ed) +* 1.8.x (EOL'ed) ## Contributing to Rambling Trie -Take a look at the [contributing guide][rambling_trie_contributing_guide] to get started, or fire a question to [@gonzedge][github_user_gonzedge]. +Take a look at the [contributing guide][rambling_trie_contributing_guide] to get started, or fire a question +to [@gonzedge][github_user_gonzedge]. ## License and copyright -Copyright (c) 2012-2017 Edgar Gonzalez +Copyright (c) 2012-2023 Edgar González MIT License -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: +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 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. +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. -[badge_fury_badge]: https://badge.fury.io/rb/rambling-trie.svg +[badge_fury_badge]: https://badge.fury.io/rb/rambling-trie.svg?version=2.3.1 [badge_fury_link]: https://badge.fury.io/rb/rambling-trie [chruby]: https://github.com/postmodern/chruby -[code_climage_link]: https://codeclimate.com/github/gonzedge/rambling-trie -[code_climate_badge]: https://codeclimate.com/github/gonzedge/rambling-trie/badges/gpa.svg +[code_climate_grade_badge]: https://codeclimate.com/github/gonzedge/rambling-trie/badges/gpa.svg +[code_climate_issues_badge]: https://codeclimate.com/github/gonzedge/rambling-trie/badges/issue_count.svg +[code_climate_link]: https://codeclimate.com/github/gonzedge/rambling-trie [coveralls_badge]: https://img.shields.io/coveralls/gonzedge/rambling-trie.svg [coveralls_link]: https://coveralls.io/r/gonzedge/rambling-trie +[downloads_badge]: https://img.shields.io/gem/dt/rambling-trie.svg +[downloads_link]: https://rubygems.org/gems/rambling-trie [gemnasium_badge]: https://gemnasium.com/gonzedge/rambling-trie.svg [gemnasium_link]: https://gemnasium.com/gonzedge/rambling-trie +[github_action_build_badge]: https://github.com/gonzedge/rambling-trie/actions/workflows/ruby.yml/badge.svg +[github_action_build_link]: https://github.com/gonzedge/rambling-trie/actions/workflows/ruby.yml +[github_action_codeql_badge]: https://github.com/gonzedge/rambling-trie/actions/workflows/codeql.yml/badge.svg +[github_action_codeql_link]: https://github.com/gonzedge/rambling-trie/actions/workflows/codeql.yml [github_user_gonzedge]: https://github.com/gonzedge [inch_ci_badge]: https://inch-ci.org/github/gonzedge/rambling-trie.svg?branch=master -[inch_ci_link]: https://inch-ci.org/github/gonzedge/rambling-trie -[license_badge]: https://badges.frapsoft.com/os/mit/mit.svg?v=103 +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg [license_link]: https://opensource.org/licenses/mit-license.php -[marshal]: https://ruby-doc.org/core-2.5.0/Marshal.html +[marshal]: https://ruby-doc.org/core-2.7.0/Marshal.html [rambling_trie_configuration]: https://github.com/gonzedge/rambling-trie#configuration [rambling_trie_contributing_guide]: https://github.com/gonzedge/rambling-trie/blob/master/CONTRIBUTING.md [rambling_trie_plain_text_reader]: https://github.com/gonzedge/rambling-trie/blob/master/lib/rambling/trie/readers/plain_text.rb @@ -295,7 +338,5 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI [rubydoc_github]: http://rubydoc.info/github/gonzedge/rambling-trie [rubyzip]: https://github.com/rubyzip/rubyzip [rvm]: https://rvm.io -[travis_ci_badge]: https://travis-ci.org/gonzedge/rambling-trie.svg -[travis_ci_link]: https://travis-ci.org/gonzedge/rambling-trie [trie_wiki]: https://en.wikipedia.org/wiki/Trie -[yaml]: https://ruby-doc.org/stdlib-2.5.0/libdoc/yaml/rdoc/YAML.html +[yaml]: https://ruby-doc.org/stdlib-2.7.0/libdoc/yaml/rdoc/YAML.html diff --git a/lib/rambling/trie.rb b/lib/rambling/trie.rb index 3319d5e8..04c8fe23 100644 --- a/lib/rambling/trie.rb +++ b/lib/rambling/trie.rb @@ -1,21 +1,20 @@ # frozen_string_literal: true %w( - comparable compressible compressor configuration container enumerable - inspectable invalid_operation readers serializers stringifyable nodes - version + comparable compressible compressor configuration container enumerable inspectable invalid_operation + readers serializers stringifyable nodes version ).each do |file| require File.join('rambling', 'trie', file) end # General namespace for all Rambling gems. module Rambling - # Entry point for rambling-trie API. + # Entry point for +rambling-trie+ API. module Trie class << self - # Creates a new Rambling::Trie. Entry point for the Rambling::Trie API. + # Creates a new +Rambling::Trie+. Entry point for the +rambling-trie+ API. # @param [String, nil] filepath the file to load the words from. - # @param [Reader, nil] reader the file parser to get each word. + # @param [Readers::Reader, nil] reader the file parser to get each word. # @return [Container] the trie just created. # @yield [Container] the trie just created. # @see Rambling::Trie::Readers Readers. @@ -23,8 +22,10 @@ def create filepath = nil, reader = nil root = root_builder.call Rambling::Trie::Container.new root, compressor do |container| + # noinspection RubyMismatchedArgumentType if filepath reader ||= readers.resolve filepath + # noinspection RubyMismatchedArgumentType,RubyNilAnalysis reader.each_word filepath do |word| container << word end @@ -36,18 +37,15 @@ def create filepath = nil, reader = nil # Loads an existing trie from disk into memory. By default, it will # deduce the correct way to deserialize based on the file extension. - # Available formats are `yml`, `marshal`, and `zip` versions of all the + # Available formats are +yml+, +marshal+, and +zip+ versions of all the # previous formats. You can also define your own. # @param [String] filepath the file to load the words from. - # @param [Serializer, nil] serializer the object responsible of loading - # the trie from disk + # @param [Serializer, nil] serializer the object responsible of loading the trie from disk. # @return [Container] the trie just loaded. # @yield [Container] the trie just loaded. # @see Rambling::Trie::Serializers Serializers. - # @note Use of - # {https://ruby-doc.org/core-2.5.0/Marshal.html#method-c-load - # Marshal.load} is generally discouraged. Only use the `.marshal` - # format with trusted input. + # @note Use of # {https://ruby-doc.org/core-2.7.0/Marshal.html#method-c-load Marshal.load} is generally + # discouraged. Only use the +.marshal+ format with trusted input. def load filepath, serializer = nil serializer ||= serializers.resolve filepath root = serializer.load filepath @@ -58,23 +56,22 @@ def load filepath, serializer = nil # Dumps an existing trie from memory into disk. By default, it will # deduce the correct way to serialize based on the file extension. - # Available formats are `yml`, `marshal`, and `zip` versions of all the + # Available formats are +yml+, +marshal+, and +zip+ versions of all the # previous formats. You can also define your own. # @param [Container] trie the trie to dump into disk. # @param [String] filepath the file to dump to serialized trie into. - # @param [Serializer, nil] serializer the object responsible of - # serializing and dumping the trie into disk. - # @see Rambling::Trie::Serializers Serializers. + # @param [Serializers::Serializer, nil] serializer the object responsible for trie serialization. + # @return [void] + # @see Serializers Serializers. def dump trie, filepath, serializer = nil serializer ||= serializers.resolve filepath + # noinspection RubyNilAnalysis serializer.dump trie.root, filepath end - # Provides configuration properties for the Rambling::Trie gem. - # @return [Configuration::Properties] the configured properties of the - # gem. - # @yield [Configuration::Properties] the configured properties of the - # gem. + # Provides configuration properties for the +Rambling::Trie+ gem. + # @return [Configuration::Properties] the configured properties of the gem. + # @yield [Configuration::Properties] the configured properties of the gem. def config yield properties if block_given? properties diff --git a/lib/rambling/trie/comparable.rb b/lib/rambling/trie/comparable.rb index 3a3b664d..a5678b80 100644 --- a/lib/rambling/trie/comparable.rb +++ b/lib/rambling/trie/comparable.rb @@ -6,9 +6,8 @@ module Trie module Comparable # Compares two nodes. # @param [Nodes::Node] other the node to compare against. - # @return [Boolean] `true` if the nodes' {Nodes::Node#letter #letter} and - # {Nodes::Node#children_tree #children_tree} are equal, `false` - # otherwise. + # @return [Boolean] +true+ if the nodes' {Nodes::Node#letter #letter} and + # {Nodes::Node#children_tree #children_tree} are equal, +false+ otherwise. def == other letter == other.letter && terminal? == other.terminal? && diff --git a/lib/rambling/trie/compressible.rb b/lib/rambling/trie/compressible.rb index 2b3faa43..40324efe 100644 --- a/lib/rambling/trie/compressible.rb +++ b/lib/rambling/trie/compressible.rb @@ -4,12 +4,10 @@ module Rambling module Trie # Provides the compressible behavior for the trie data structure. module Compressible - # Indicates if the current {Rambling::Trie::Nodes::Node Node} can be - # compressed or not. - # @return [Boolean] `true` for non-{Nodes::Node#terminal? terminal} nodes - # with one child, `false` otherwise. + # Indicates if the current {Rambling::Trie::Nodes::Node Node} can be compressed or not. + # @return [Boolean] +true+ for non-{Nodes::Node#terminal? terminal} nodes with one child, +false+ otherwise. def compressible? - !(root? || terminal?) && children_tree.size == 1 + !(root? || terminal?) && 1 == children_tree.size end end end diff --git a/lib/rambling/trie/compressor.rb b/lib/rambling/trie/compressor.rb index 810442a3..9bced889 100644 --- a/lib/rambling/trie/compressor.rb +++ b/lib/rambling/trie/compressor.rb @@ -24,21 +24,11 @@ def compress_child_and_merge node def merge node, other letter = node.letter.to_s << other.letter.to_s - new_compressed_node( - letter.to_sym, - node.parent, - other.children_tree, - other.terminal?, - ) + new_compressed_node letter.to_sym, node.parent, other.children_tree, other.terminal? end def compress_children_and_copy node - new_compressed_node( - node.letter, - node.parent, - compress_children(node.children_tree), - node.terminal?, - ) + new_compressed_node node.letter, node.parent, compress_children(node.children_tree), node.terminal? end def compress_children tree diff --git a/lib/rambling/trie/configuration/properties.rb b/lib/rambling/trie/configuration/properties.rb index 6ea7617a..a40e45db 100644 --- a/lib/rambling/trie/configuration/properties.rb +++ b/lib/rambling/trie/configuration/properties.rb @@ -6,26 +6,23 @@ module Configuration # Provides configurable properties for Rambling::Trie. class Properties # The configured {Readers Readers}. - # @return [ProviderCollection] the mapping of configured {Readers - # Readers}. + # @return [ProviderCollection] the mapping of configured {Readers Readers}. attr_reader :readers # The configured {Serializers Serializers}. - # @return [ProviderCollection] the mapping of configured {Serializers - # Serializers}. + # @return [ProviderCollection] the mapping of configured {Serializers Serializers}. attr_reader :serializers # The configured {Compressor Compressor}. # @return [Compressor] the configured compressor. attr_accessor :compressor - # The configured root_builder, which should return a {Nodes::Node Node} - # when called. - # @return [Proc] the configured root_builder. + # The configured +root_builder+, which returns a {Nodes::Node Node} when called. + # @return [Proc] the configured +root_builder+. attr_accessor :root_builder - # The configured tmp_path, which will be used for throwaway files. - # @return [String] the configured tmp_path. + # The configured +tmp_path+, which will be used for throwaway files. + # @return [String] the configured +tmp_path+. attr_accessor :tmp_path # Returns a new properties instance. @@ -34,6 +31,7 @@ def initialize end # Resets back to default properties. + # @return [void] def reset reset_readers reset_serializers @@ -49,11 +47,7 @@ def reset def reset_readers plain_text_reader = Rambling::Trie::Readers::PlainText.new - - @readers = Rambling::Trie::Configuration::ProviderCollection.new( - :reader, - txt: plain_text_reader, - ) + @readers = Rambling::Trie::Configuration::ProviderCollection.new :reader, txt: plain_text_reader end def reset_serializers diff --git a/lib/rambling/trie/configuration/provider_collection.rb b/lib/rambling/trie/configuration/provider_collection.rb index ccba8eca..7e827433 100644 --- a/lib/rambling/trie/configuration/provider_collection.rb +++ b/lib/rambling/trie/configuration/provider_collection.rb @@ -6,7 +6,7 @@ module Configuration # Collection of configurable providers. class ProviderCollection # The name of this provider collection. - # @return [String] the name of this provider collection. + # @return [Symbol] the name of this provider collection. attr_reader :name # @overload default @@ -15,18 +15,17 @@ class ProviderCollection # @overload default=(provider) # Sets the default provider. Needs to be one of the configured # providers. - # @param [Object] provider the provider to use as default. - # @raise [ArgumentError] when the given provider is not in the - # provider collection. - # @note If no providers have been configured, `nil` will be assigned. - # @return [Object, nil] the default provider to use when a provider - # cannot be resolved in {ProviderCollection#resolve #resolve}. + # @param [TProvider] provider the provider to use as default. + # @raise [ArgumentError] when the given provider is not in the provider collection. + # @note If no providers have been configured, +nil+ will be assigned. + # @return [TProvider, nil] the default provider to use when a provider cannot be resolved in + # {ProviderCollection#resolve #resolve}. attr_reader :default # Creates a new provider collection. - # @param [String] name the name for this provider collection. - # @param [Hash] providers the configured providers. - # @param [Object] default the configured default provider. + # @param [Symbol] name the name for this provider collection. + # @param [Hash] providers the configured providers. + # @param [TProvider, nil] default the configured default provider. def initialize name, providers = {}, default = nil @name = name @configured_providers = providers @@ -36,39 +35,34 @@ def initialize name, providers = {}, default = nil end # Adds a new provider to the provider collection. - # @param [Symbol] extension the extension that the provider will - # correspond to. - # @param [provider] provider the provider to add to the provider - # collection. + # @param [Symbol] extension the extension that the provider will correspond to. + # @param [TProvider] provider the provider to add to the provider collection. + # @return [TProvider] the provider just added. def add extension, provider providers[extension] = provider end def default= provider - unless contains? provider - raise ArgumentError, - "default #{name} should be part of configured #{name}s" - end + raise ArgumentError, "default #{name} should be part of configured #{name}s" unless contains? provider @default = provider end # List of configured providers. - # @return [Hash] the mapping of extensions to their corresponding - # providers. + # @return [Hash] the mapping of extensions to their corresponding providers. def providers @providers ||= {} end # Resolves the provider from a filepath based on the file extension. # @param [String] filepath the filepath to resolve into a provider. - # @return [Object] the provider corresponding to the file extension in - # this provider collection. {#default} if not found. + # @return [TProvider, nil] the provider for the given file's extension. {#default} if not found. def resolve filepath providers[file_format filepath] || default end # Resets the provider collection to the initial values. + # @return [void] def reset providers.clear configured_providers.each { |k, v| self[k] = v } @@ -77,7 +71,7 @@ def reset # Get provider corresponding to a given format. # @return [Array] the provider corresponding to that format. - # @see https://ruby-doc.org/core-2.5.0/Hash.html#method-i-5B-5D + # @see https://ruby-doc.org/core-2.7.0/Hash.html#method-i-5B-5D # Hash#keys def formats providers.keys @@ -85,9 +79,8 @@ def formats # Get provider corresponding to a given format. # @param [Symbol] format the format to search for in the collection. - # @return [Object] the provider corresponding to that format. - # @see https://ruby-doc.org/core-2.5.0/Hash.html#method-i-5B-5D - # Hash#[] + # @return [TProvider] the provider corresponding to that format. + # @see https://ruby-doc.org/core-2.7.0/Hash.html#method-i-5B-5D Hash#[] def [] format providers[format] end @@ -111,8 +104,7 @@ def file_format filepath end def contains? provider - provider.nil? || - (providers.any? && provider_instances.include?(provider)) + provider.nil? || (providers.any? && provider_instances.include?(provider)) end alias_method :provider_instances, :values diff --git a/lib/rambling/trie/container.rb b/lib/rambling/trie/container.rb index 15a529d6..85700496 100644 --- a/lib/rambling/trie/container.rb +++ b/lib/rambling/trie/container.rb @@ -13,7 +13,7 @@ class Container # Creates a new trie. # @param [Nodes::Node] root the root node for the trie # @param [Compressor] compressor responsible for compressing the trie - # @yield [Container] the trie just created. + # @yield [self] the trie just initialized. def initialize root, compressor @root = root @compressor = compressor @@ -41,72 +41,61 @@ def concat words words.map { |word| add word } end - # Compresses the existing trie using redundant node elimination. Marks - # the trie as compressed. Does nothing if the trie has already been - # compressed. - # @return [Container] self - # @note This method replaces the root {Nodes::Raw Raw} node with a - # {Nodes::Compressed Compressed} version of it. + # Compresses the existing trie using redundant node elimination. + # Marks the trie as compressed. + # Does nothing if the trie has already been compressed. + # @return [self] + # @note This method replaces the root {Nodes::Raw Raw} node with a {Nodes::Compressed Compressed} version of it. def compress! self.root = compress_root unless root.compressed? self end - # Compresses the existing trie using redundant node elimination. Returns - # a new trie with the compressed root. - # @return [Container] A new {Container} with the {Nodes::Compressed - # Compressed} root node or self if the trie has already been - # compressed. + # Compresses the existing trie using redundant node elimination. Returns a new trie with the compressed root. + # @return [Container] A new {Container} with the {Nodes::Compressed Compressed} root node + # or self if the trie has already been compressed. def compress return self if root.compressed? + Rambling::Trie::Container.new compress_root, compressor end # Checks if a path for a word or partial word exists in the trie. # @param [String] word the word or partial word to look for in the trie. - # @return [Boolean] `true` if the word or partial word is found, `false` - # otherwise. - # @see Nodes::Raw#partial_word? - # @see Nodes::Compressed#partial_word? + # @return [Boolean] +true+ if the word or partial word is found, +false+ otherwise. + # @see Nodes::Node#partial_word? def partial_word? word = '' root.partial_word? word.chars end # Checks if a whole word exists in the trie. # @param [String] word the word to look for in the trie. - # @return [Boolean] `true` only if the word is found and the last - # character corresponds to a terminal node, `false` otherwise. - # @see Nodes::Raw#word? - # @see Nodes::Compressed#word? + # @return [Boolean] +true+ only if the word is found and the last character corresponds to a terminal node, + # +false+ otherwise. + # @see Nodes::Node#word? def word? word = '' root.word? word.chars end # Returns all words that start with the specified characters. # @param [String] word the word to look for in the trie. - # @return [Array] all the words contained in the trie that start - # with the specified characters. - # @see Nodes::Raw#scan - # @see Nodes::Compressed#scan + # @return [Array] all the words contained in the trie that start with the specified characters. + # @see Nodes::Node#scan def scan word = '' root.scan(word.chars).to_a end - # Returns all words within a string that match a word contained in the - # trie. + # Returns all words within a string that match a word contained in the trie. # @param [String] phrase the string to look for matching words in. - # @return [Enumerator] all the words in the given string that - # match a word in the trie. + # @return [Enumerator] all the words in the given string that match a word in the trie. # @yield [String] each word found in phrase. - # @see Nodes::Node#words_within def words_within phrase words_within_root(phrase).to_a end # Checks if there are any valid words in a given string. # @param [String] phrase the string to look for matching words in. - # @return [Boolean] `true` if any word within phrase is contained in the - # trie, `false` otherwise. + # @return [Boolean] +true+ if any word within phrase is contained in the trie, +false+ otherwise. # @see Container#words_within def words_within? phrase words_within_root(phrase).any? @@ -114,13 +103,14 @@ def words_within? phrase # Compares two trie data structures. # @param [Container] other the trie to compare against. - # @return [Boolean] `true` if the tries are equal, `false` otherwise. + # @return [Boolean] +true+ if the tries are equal, +false+ otherwise. def == other root == other.root end # Iterates over the words contained in the trie. # @yield [String] the words contained in this trie node. + # @return [self] def each return enum_for :each unless block_given? @@ -143,33 +133,29 @@ def [] letter end # Root node's child nodes. - # @return [Array] the array of children nodes contained in - # the root node. + # @return [Array] the array of children nodes contained in the root node. # @see Nodes::Node#children def children root.children end # Root node's children tree. - # @return [Array] the array of children nodes contained in - # the root node. + # @return [Hash] the children tree hash contained in the root node, consisting of + # +:letter => node+. # @see Nodes::Node#children_tree def children_tree root.children_tree end - # Indicates if the root {Nodes::Node Node} can be - # compressed or not. - # @return [Boolean] `true` for non-{Nodes::Node#terminal? terminal} - # nodes with one child, `false` otherwise. + # Indicates if the root {Nodes::Node Node} can be compressed or not. + # @return [Boolean] +true+ for non-{Nodes::Node#terminal? terminal} nodes with one child, +false+ otherwise. def compressed? root.compressed? end # Array of words contained in the root {Nodes::Node Node}. # @return [Array] all words contained in this trie. - # @see https://ruby-doc.org/core-2.5.0/Enumerable.html#method-i-to_a - # Enumerable#to_a + # @see https://ruby-doc.org/core-2.7.0/Enumerable.html#method-i-to_a Enumerable#to_a def to_a root.to_a end diff --git a/lib/rambling/trie/enumerable.rb b/lib/rambling/trie/enumerable.rb index 3a1b5333..b3ca64f4 100644 --- a/lib/rambling/trie/enumerable.rb +++ b/lib/rambling/trie/enumerable.rb @@ -7,12 +7,12 @@ module Enumerable include ::Enumerable # Returns number of words contained in the trie - # @see https://ruby-doc.org/core-2.5.0/Enumerable.html#method-i-count - # Enumerable#count + # @see https://ruby-doc.org/core-2.7.0/Enumerable.html#method-i-count Enumerable#count alias_method :size, :count # Iterates over the words contained in the trie. # @yield [String] the words contained in this trie node. + # @return [self] def each return enum_for :each unless block_given? @@ -23,6 +23,8 @@ def each yield word end end + + self end end end diff --git a/lib/rambling/trie/nodes/compressed.rb b/lib/rambling/trie/nodes/compressed.rb index 92ebb81b..5c901c22 100644 --- a/lib/rambling/trie/nodes/compressed.rb +++ b/lib/rambling/trie/nodes/compressed.rb @@ -9,14 +9,13 @@ class Compressed < Rambling::Trie::Nodes::Node # trying to add a word to the current compressed trie node # @param [String] _ the word to add to the trie. # @raise [InvalidOperation] if the trie is already compressed. - # @return [nil] this never returns as it always raises an exception. + # @return [void] def add _ - raise Rambling::Trie::InvalidOperation, - 'Cannot add word to compressed trie' + raise Rambling::Trie::InvalidOperation, 'Cannot add word to compressed trie' end - # Always return `true` for a compressed node. - # @return [Boolean] always `true` for a compressed node. + # Always return +true+ for a compressed node. + # @return [Boolean] always +true+ for a compressed node. def compressed? true end diff --git a/lib/rambling/trie/nodes/missing.rb b/lib/rambling/trie/nodes/missing.rb index 17c3bb1e..b0892e61 100644 --- a/lib/rambling/trie/nodes/missing.rb +++ b/lib/rambling/trie/nodes/missing.rb @@ -3,8 +3,7 @@ module Rambling module Trie module Nodes - # A representation of a missing node in the trie data structure. Returned - # when a node is not found. + # A representation of a missing node in the trie data structure. Returned when a node is not found. class Missing < Rambling::Trie::Nodes::Node end end diff --git a/lib/rambling/trie/nodes/node.rb b/lib/rambling/trie/nodes/node.rb index 2c7975ae..4163e9d5 100644 --- a/lib/rambling/trie/nodes/node.rb +++ b/lib/rambling/trie/nodes/node.rb @@ -22,8 +22,7 @@ class Node attr_reader :letter # Child nodes tree. - # @return [Hash] the children_tree hash, consisting of `:letter => - # node`. + # @return [Hash] the children tree hash, consisting of +:letter => node+. attr_accessor :children_tree # Parent node. @@ -31,7 +30,7 @@ class Node attr_accessor :parent # Creates a new node. - # @param [Symbol, nil] letter the Node's letter value + # @param [Symbol, nil] letter the Node's letter value. # @param [Node, nil] parent the parent of the current node. def initialize letter = nil, parent = nil, children_tree = {} @letter = letter @@ -40,8 +39,7 @@ def initialize letter = nil, parent = nil, children_tree = {} end # Child nodes. - # @return [Array] the array of children nodes contained - # in the current node. + # @return [Array] the array of child nodes contained in the current node. def children children_tree.values end @@ -51,26 +49,25 @@ def children def first_child return if children_tree.empty? - children_tree.each_value do |child| - return child - end + # rubocop:disable Lint/UnreachableLoop + children_tree.each_value { |child| return child } + # rubocop:enable Lint/UnreachableLoop end # Indicates if the current node is the root node. - # @return [Boolean] `true` if the node does not have a parent, `false` - # otherwise. + # @return [Boolean] +true+ if the node does not have a parent, +false+ otherwise. def root? !parent end # Indicates if a {Node Node} is terminal or not. - # @return [Boolean] `true` for terminal nodes, `false` otherwise. + # @return [Boolean] +true+ for terminal nodes, +false+ otherwise. def terminal? !!terminal end # Mark {Node Node} as terminal. - # @return [Node] the modified node. + # @return [self] the modified node. def terminal! self.terminal = true self @@ -82,8 +79,7 @@ def letter= letter # Checks if a path for a set of characters exists in the trie. # @param [Array] chars the characters to look for in the trie. - # @return [Boolean] `true` if the characters are found, `false` - # otherwise. + # @return [Boolean] +true+ if the characters are found, +false+ otherwise. def partial_word? chars return true if chars.empty? @@ -92,8 +88,7 @@ def partial_word? chars # Checks if a path for set of characters represents a word in the trie. # @param [Array] chars the characters to look for in the trie. - # @return [Boolean] `true` if the characters are found and form a word, - # `false` otherwise. + # @return [Boolean] +true+ if the characters are found and form a word, +false+ otherwise. def word? chars = [] return terminal? if chars.empty? @@ -112,8 +107,7 @@ def scan chars # Returns all words that match a prefix of any length within chars. # @param [String] chars the chars to base the prefix on. - # @return [Enumerator] all the words that match a prefix given - # by chars. + # @return [Enumerator] all the words that match a prefix by chars. # @yield [String] each word found. def match_prefix chars return enum_for :match_prefix, chars unless block_given? @@ -128,8 +122,7 @@ def match_prefix chars # Get {Node Node} corresponding to a given letter. # @param [Symbol] letter the letter to search for in the node. # @return [Node] the node corresponding to that letter. - # @see https://ruby-doc.org/core-2.5.0/Hash.html#method-i-5B-5D - # Hash#[] + # @see https://ruby-doc.org/core-2.7.0/Hash.html#method-i-5B-5D Hash#[] def [] letter children_tree[letter] end @@ -137,31 +130,25 @@ def [] letter # Set the {Node Node} that corresponds to a given letter. # @param [Symbol] letter the letter to insert or update in the node's # @param [Node] node the {Node Node} to assign to that letter. - # @return [Node] the node corresponding to the inserted or - # updated letter. - # @see https://ruby-doc.org/core-2.5.0/Hash.html#method-i-5B-5D - # Hash#[] + # @return [Node] the node corresponding to the inserted or updated letter. + # @see https://ruby-doc.org/core-2.7.0/Hash.html#method-i-5B-5D Hash#[] def []= letter, node children_tree[letter] = node end - # Check if a {Node Node}'s children tree contains a given - # letter. + # Check if a {Node Node}'s children tree contains a given letter. # @param [Symbol] letter the letter to search for in the node. - # @return [Boolean] `true` if the letter is present, `false` otherwise - # @see https://ruby-doc.org/core-2.5.0/Hash.html#method-i-has_key-3F - # Hash#key? + # @return [Boolean] +true+ if the letter is present, +false+ otherwise. + # @see https://ruby-doc.org/core-2.7.0/Hash.html#method-i-has_key-3F Hash#key? def key? letter children_tree.key? letter end # Delete a given letter and its corresponding {Node Node} from - # this {Node Node}'s children tree. - # @param [Symbol] letter the letter to delete from the node's children - # tree. + # this {Node Node}'s children tree. + # @param [Symbol] letter the letter to delete from the node's children tree. # @return [Node] the node corresponding to the deleted letter. - # @see https://ruby-doc.org/core-2.5.0/Hash.html#method-i-delete - # Hash#delete + # @see https://ruby-doc.org/core-2.7.0/Hash.html#method-i-delete Hash#delete def delete letter children_tree.delete letter end diff --git a/lib/rambling/trie/nodes/raw.rb b/lib/rambling/trie/nodes/raw.rb index 3a177e75..95f8a984 100644 --- a/lib/rambling/trie/nodes/raw.rb +++ b/lib/rambling/trie/nodes/raw.rb @@ -17,8 +17,8 @@ def add chars end end - # Always return `false` for a raw (uncompressed) node. - # @return [Boolean] always `false` for a raw (uncompressed) node. + # Always return +false+ for a raw (uncompressed) node. + # @return [Boolean] always +false+ for a raw (uncompressed) node. def compressed? false end diff --git a/lib/rambling/trie/readers.rb b/lib/rambling/trie/readers.rb index cd052f25..f5ed5f65 100644 --- a/lib/rambling/trie/readers.rb +++ b/lib/rambling/trie/readers.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -%w(plain_text).each do |file| +%w(reader plain_text).each do |file| require File.join('rambling', 'trie', 'readers', file) end diff --git a/lib/rambling/trie/readers/plain_text.rb b/lib/rambling/trie/readers/plain_text.rb index 8ab4142a..29869766 100644 --- a/lib/rambling/trie/readers/plain_text.rb +++ b/lib/rambling/trie/readers/plain_text.rb @@ -3,14 +3,18 @@ module Rambling module Trie module Readers - # File reader for .txt files. - class PlainText - # Yields each word read from a .txt file. - # @param [String] filepath the full path of the file to load the words - # from. + # File reader for +.txt+ files. + class PlainText < Reader + # Yields each word read from a +.txt+ file. + # @param [String] filepath the full path of the file to load the words from. # @yield [String] Each line read from the file. + # @return [self] def each_word filepath - File.foreach(filepath) { |line| yield line.chomp! } + return enum_for :each_word unless block_given? + + ::File.foreach(filepath) { |line| yield line.chomp! } + + self end end end diff --git a/lib/rambling/trie/readers/reader.rb b/lib/rambling/trie/readers/reader.rb new file mode 100644 index 00000000..727d6770 --- /dev/null +++ b/lib/rambling/trie/readers/reader.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Rambling + module Trie + module Readers + # Base class for all readers. + class Reader + # Yields each word read from given file. + # @abstract Subclass and override {#each_word} to fit to a particular file format. + # @param [String] filepath the full path of the file to load the words from. + # @yield [String] Each line read from the file. + # @return [self] + def each_word filepath + raise NotImplementedError + end + end + end + end +end diff --git a/lib/rambling/trie/serializers.rb b/lib/rambling/trie/serializers.rb index 9c990bd1..bba09e96 100644 --- a/lib/rambling/trie/serializers.rb +++ b/lib/rambling/trie/serializers.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -%w(file marshal yaml zip).each do |file| +%w(serializer file marshal yaml zip).each do |file| require File.join('rambling', 'trie', 'serializers', file) end diff --git a/lib/rambling/trie/serializers/file.rb b/lib/rambling/trie/serializers/file.rb index de5a1d8b..5fc6db22 100644 --- a/lib/rambling/trie/serializers/file.rb +++ b/lib/rambling/trie/serializers/file.rb @@ -4,7 +4,7 @@ module Rambling module Trie module Serializers # Basic file serializer. Dumps/loads string contents from files. - class File + class File < Serializer # Loads contents from a specified filepath. # @param [String] filepath the filepath to load contents from. # @return [String] all contents of the file. diff --git a/lib/rambling/trie/serializers/marshal.rb b/lib/rambling/trie/serializers/marshal.rb index 568c1b3f..758ad099 100644 --- a/lib/rambling/trie/serializers/marshal.rb +++ b/lib/rambling/trie/serializers/marshal.rb @@ -3,38 +3,30 @@ module Rambling module Trie module Serializers - # Serializer for Ruby marshal format (.marshal) files. - class Marshal + # Serializer for Ruby marshal format (+.marshal+) files. + class Marshal < Serializer # Creates a new Marshal serializer. - # @param [Serializer] serializer the serializer responsible to write to - # and read from disk. + # @param [Serializer] serializer the serializer responsible to write to and read from disk. def initialize serializer = nil + super() @serializer = serializer || Rambling::Trie::Serializers::File.new end - # Loads marshaled object from contents in filepath and deserializes it - # into a {Nodes::Node Node}. - # @param [String] filepath the full path of the file to load the - # marshaled object from. + # Loads marshaled object from contents in filepath and deserializes it into a {Nodes::Node Node}. + # @param [String] filepath the full path of the file to load the marshaled object from. # @return [Nodes::Node] The deserialized {Nodes::Node Node}. - # @see https://ruby-doc.org/core-2.5.0/Marshal.html#method-c-load - # Marshal.load - # @note Use of - # {https://ruby-doc.org/core-2.5.0/Marshal.html#method-c-load - # Marshal.load} is generally discouraged. Only use this with trusted - # input. + # @see https://ruby-doc.org/core-2.7.0/Marshal.html#method-c-load Marshal.load + # @note Use of {https://ruby-doc.org/core-2.7.0/Marshal.html#method-c-load Marshal.load} is generally + # discouraged. Only use this with trusted input. def load filepath ::Marshal.load serializer.load filepath end - # Serializes a {Nodes::Node Node} and dumps it as a marshaled object - # into filepath. + # Serializes a {Nodes::Node Node} and dumps it as a marshaled object into filepath. # @param [Nodes::Node] node the node to serialize - # @param [String] filepath the full path of the file to dump the - # marshaled object into. + # @param [String] filepath the full path of the file to dump the marshaled object into. # @return [Numeric] number of bytes written to disk. - # @see https://ruby-doc.org/core-2.5.0/Marshal.html#method-c-dump - # Marshal.dump + # @see https://ruby-doc.org/core-2.7.0/Marshal.html#method-c-dump Marshal.dump def dump node, filepath serializer.dump ::Marshal.dump(node), filepath end diff --git a/lib/rambling/trie/serializers/serializer.rb b/lib/rambling/trie/serializers/serializer.rb new file mode 100644 index 00000000..f9f961d4 --- /dev/null +++ b/lib/rambling/trie/serializers/serializer.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Rambling + module Trie + module Serializers + # Base class for all serializers. + class Serializer + # Loads contents from a specified filepath. + # @abstract Subclass and override {#load} to parse the desired format. + # @param [String] filepath the filepath to load contents from. + # @return [TContents] parsed contents from given file. + def load filepath + raise NotImplementedError + end + + # Dumps contents into a specified filepath. + # @abstract Subclass and override {#dump} to output the desired format. + # @param [TContents] contents the contents to dump into given file. + # @param [String] filepath the filepath to dump the contents to. + # @return [Numeric] number of bytes written to disk. + def dump contents, filepath + raise NotImplementedError + end + end + end + end +end diff --git a/lib/rambling/trie/serializers/yaml.rb b/lib/rambling/trie/serializers/yaml.rb index 42059483..fc0a8862 100644 --- a/lib/rambling/trie/serializers/yaml.rb +++ b/lib/rambling/trie/serializers/yaml.rb @@ -3,44 +3,37 @@ module Rambling module Trie module Serializers - # Serializer for Ruby yaml format (.yaml) files. - class Yaml + # Serializer for Ruby yaml format (+.yaml+, or +.yml+) files. + class Yaml < Serializer # Creates a new Yaml serializer. - # @param [Serializer] serializer the serializer responsible to write to - # and read from disk. + # @param [Serializer] serializer the serializer responsible to write to and read from disk. def initialize serializer = nil + super() @serializer = serializer || Rambling::Trie::Serializers::File.new end - # Loads serialized object from YAML file in filepath and deserializes - # it into a {Nodes::Node Node}. - # @param [String] filepath the full path of the file to load the - # serialized YAML object from. + # Loads serialized object from YAML file in filepath and deserializes it into a {Nodes::Node Node}. + # @param [String] filepath the full path of the file to load the serialized YAML object from. # @return [Nodes::Node] The deserialized {Nodes::Node Node}. - # @see https://ruby-doc.org/stdlib-2.5.0/libdoc/psych/rdoc/Psych.html#method-c-safe_load - # Psych.safe_load + # @see https://ruby-doc.org/stdlib-2.7.0/libdoc/psych/rdoc/Psych.html#method-c-safe_load Psych.safe_load def load filepath require 'yaml' ::YAML.safe_load( serializer.load(filepath), - [ + permitted_classes: [ Symbol, Rambling::Trie::Nodes::Raw, Rambling::Trie::Nodes::Compressed, ], - [], - true, + aliases: true, ) end - # Serializes a {Nodes::Node Node} and dumps it as a YAML object into - # filepath. + # Serializes a {Nodes::Node Node} and dumps it as a YAML object into filepath. # @param [Nodes::Node] node the node to serialize - # @param [String] filepath the full path of the file to dump the YAML - # object into. + # @param [String] filepath the full path of the file to dump the YAML object into. # @return [Numeric] number of bytes written to disk. - # @see https://ruby-doc.org/stdlib-2.5.0/libdoc/psych/rdoc/Psych.html#method-c-dump - # Psych.dump + # @see https://ruby-doc.org/stdlib-2.7.0/libdoc/psych/rdoc/Psych.html#method-c-dump Psych.dump def dump node, filepath require 'yaml' serializer.dump ::YAML.dump(node), filepath diff --git a/lib/rambling/trie/serializers/zip.rb b/lib/rambling/trie/serializers/zip.rb index bf8ee68d..2f01886c 100644 --- a/lib/rambling/trie/serializers/zip.rb +++ b/lib/rambling/trie/serializers/zip.rb @@ -3,20 +3,22 @@ module Rambling module Trie module Serializers - # Zip file serializer. Dumps/loads contents from zip files. Automatically - # detects if zip file contains `.marshal` or `.yml` file - class Zip + # Zip file serializer. Dumps/loads contents from +.zip+ files. + # Automatically detects if zip file contains a +.marshal+ or +.yml+ file, + # or any other registered +:format => serializer+ combo. + class Zip < Serializer # Creates a new Zip serializer. # @param [Configuration::Properties] properties the configuration # properties set up so far. def initialize properties + super() @properties = properties end # Unzip contents from specified filepath and load in contents from # unzipped files. # @param [String] filepath the filepath to load contents from. - # @return [String] all contents of the unzipped loaded file. + # @return [TContents] all contents of the unzipped loaded file. # @see https://github.com/rubyzip/rubyzip#reading-a-zip-file Zip # reading a file def load filepath @@ -35,7 +37,7 @@ def load filepath # Dumps contents and zips into a specified filepath. # @param [String] contents the contents to dump. # @param [String] filepath the filepath to dump the contents to. - # @return [Numeric] number of bytes written to disk. + # @return [TContents] number of bytes written to disk. # @see https://github.com/rubyzip/rubyzip#basic-zip-archive-creation # Zip archive creation def dump contents, filepath @@ -50,6 +52,8 @@ def dump contents, filepath zip.add filename, entry_path end + + ::File.size filepath end private diff --git a/lib/rambling/trie/stringifyable.rb b/lib/rambling/trie/stringifyable.rb index 41aae797..0fb8d82f 100644 --- a/lib/rambling/trie/stringifyable.rb +++ b/lib/rambling/trie/stringifyable.rb @@ -2,7 +2,7 @@ module Rambling module Trie - # Provides the String representation behavior for the trie data structure. + # Provides the +String+ representation behavior for the trie data structure. module Stringifyable # String representation of the current node, if it is a terminal node. # @return [String] the string representation of the current node. diff --git a/lib/rambling/trie/version.rb b/lib/rambling/trie/version.rb index 17a67948..f112efb6 100644 --- a/lib/rambling/trie/version.rb +++ b/lib/rambling/trie/version.rb @@ -3,6 +3,6 @@ module Rambling module Trie # Current version of the rambling-trie. - VERSION = '2.1.2' + VERSION = '2.3.1' end end diff --git a/rambling-trie.gemspec b/rambling-trie.gemspec index 46d8daf4..bb7f83e0 100644 --- a/rambling-trie.gemspec +++ b/rambling-trie.gemspec @@ -1,11 +1,11 @@ # frozen_string_literal: true -$LOAD_PATH.push File.expand_path('../lib', __FILE__) +$LOAD_PATH.push File.expand_path('lib', __dir__) require 'rambling/trie/version' Gem::Specification.new do |gem| gem.authors = ['Edgar Gonzalez', 'Lilibeth De La Cruz'] - gem.email = ['edggonzalezg@gmail.com', 'lilibethdlc@gmail.com'] + gem.email = %w(edggonzalezg@gmail.com lilibethdlc@gmail.com) gem.description = <<~DESCRIPTION.gsub(%r{\s+}, ' ') The Rambling Trie is a Ruby implementation of the trie data structure, which @@ -13,8 +13,12 @@ Gem::Specification.new do |gem| DESCRIPTION gem.summary = 'A Ruby implementation of the trie data structure.' - gem.homepage = 'http://github.com/gonzedge/rambling-trie' + gem.homepage = 'https://github.com/gonzedge/rambling-trie' gem.date = Time.now.strftime '%Y-%m-%d' + gem.metadata = { + 'changelog_uri' => 'https://github.com/gonzedge/rambling-trie/blob/master/CHANGELOG.md', + 'documentation_uri' => 'https://www.rubydoc.info/gems/rambling-trie', + } executables = `git ls-files -- bin/*`.split "\n" files = `git ls-files -- {lib,*file,*.gemspec,LICENSE*,README*}`.split "\n" @@ -29,9 +33,9 @@ Gem::Specification.new do |gem| gem.license = 'MIT' gem.version = Rambling::Trie::VERSION gem.platform = Gem::Platform::RUBY - gem.required_ruby_version = '~> 2.4' + gem.required_ruby_version = '>= 2.7', '< 4' gem.add_development_dependency 'rake', '~> 13.0' - gem.add_development_dependency 'rspec', '~> 3.9' - gem.add_development_dependency 'yard', '~> 0.9.25' + gem.add_development_dependency 'rspec', '~> 3.12' + gem.add_development_dependency 'yard', '~> 0.9.28' end diff --git a/spec/integration/rambling/trie_spec.rb b/spec/integration/rambling/trie_spec.rb index 4fe58bf6..c578c5a3 100644 --- a/spec/integration/rambling/trie_spec.rb +++ b/spec/integration/rambling/trie_spec.rb @@ -6,32 +6,60 @@ describe Rambling::Trie do let(:assets_path) { File.join ::SPEC_ROOT, 'assets' } + describe '::VERSION' do + let(:root_path) { File.join ::SPEC_ROOT, '..' } + let(:readme_path) { File.join root_path, 'README.md' } + let(:readme) { File.read readme_path } + let(:changelog_path) { File.join root_path, 'CHANGELOG.md' } + let(:changelog) { File.read changelog_path } + + let(:changelog_versions) do + matches = [] + changelog.scan %r{^## (\d+\.\d+\.\d+)} do |match| + matches << match[0] + end + matches + end + + it 'matches with the version in the README badge' do + match = %r{\?version=(?.*)$}.match readme + expect(match['version']).to eq Rambling::Trie::VERSION + end + + it 'is the version before the one at the top of the CHANGELOG' do + changelog_version = Gem::Version.new changelog_versions.first + lib_version = Gem::Version.new "#{Rambling::Trie::VERSION}.0" + expect(changelog_version).to eq lib_version.bump + end + + it 'is included in the CHANGELOG diffs' do + changelog_versions.shift + expect(changelog_versions.first).to eq Rambling::Trie::VERSION + end + end + context 'when providing words directly' do it_behaves_like 'a compressible trie' do - let(:trie) { Rambling::Trie.create } + let(:trie) { described_class.create } let(:words) { %w(a couple of words for our full trie integration test) } - before do - trie.concat words - end + before { trie.concat words } end end context 'when provided with words with unicode characters' do it_behaves_like 'a compressible trie' do - let(:trie) { Rambling::Trie.create } - let(:words) do + let(:trie) { described_class.create } + let :words do %w(poquísimas palabras para nuestra prueba de integración completa 🙃) end - before do - trie.concat words - end + before { trie.concat words } end end context 'when provided with a filepath' do - let(:trie) { Rambling::Trie.create filepath } + let(:trie) { described_class.create filepath } let(:words) { File.readlines(filepath).map(&:chomp) } context 'with english words' do @@ -53,34 +81,35 @@ context 'when serialized with Ruby marshal format (default)' do it_behaves_like 'a serializable trie' do - let(:trie_to_serialize) { Rambling::Trie.create words_filepath } - let(:format) { :marshal } + let(:trie_to_serialize) { described_class.create words_filepath } + let(:file_format) { :marshal } end end context 'when serialized with YAML' do it_behaves_like 'a serializable trie' do - let(:trie_to_serialize) { Rambling::Trie.create words_filepath } - let(:format) { :yml } + let(:trie_to_serialize) { described_class.create words_filepath } + let(:file_format) { :yml } end end context 'when serialized with zipped Ruby marshal format' do + let!(:original_on_exists_proc) { ::Zip.on_exists_proc } + let!(:original_continue_on_exists_proc) { ::Zip.continue_on_exists_proc } + before do - @original_on_exists_proc = ::Zip.on_exists_proc - @original_continue_on_exists_proc = ::Zip.continue_on_exists_proc ::Zip.on_exists_proc = true ::Zip.continue_on_exists_proc = true end after do - ::Zip.on_exists_proc = @original_on_exists_proc - ::Zip.continue_on_exists_proc = @original_continue_on_exists_proc + ::Zip.on_exists_proc = original_on_exists_proc + ::Zip.continue_on_exists_proc = original_continue_on_exists_proc end it_behaves_like 'a serializable trie' do - let(:trie_to_serialize) { Rambling::Trie.create words_filepath } - let(:format) { 'marshal.zip' } + let(:trie_to_serialize) { described_class.create words_filepath } + let(:file_format) { 'marshal.zip' } end end end diff --git a/spec/lib/rambling/trie/comparable_spec.rb b/spec/lib/rambling/trie/comparable_spec.rb index f205c243..93e4d58e 100644 --- a/spec/lib/rambling/trie/comparable_spec.rb +++ b/spec/lib/rambling/trie/comparable_spec.rb @@ -4,93 +4,83 @@ describe Rambling::Trie::Comparable do describe '#==' do - let(:node_1) { Rambling::Trie::Nodes::Raw.new } - let(:node_2) { Rambling::Trie::Nodes::Raw.new } + let(:node_one) { Rambling::Trie::Nodes::Raw.new } + let(:node_two) { Rambling::Trie::Nodes::Raw.new } context 'when the nodes do not have the same letter' do before do - node_1.letter = :a - node_2.letter = :b + node_one.letter = :a + node_two.letter = :b end it 'returns false' do - expect(node_1).not_to eq node_2 + expect(node_one).not_to eq node_two end end - context 'when the nodes have the same letter and no children' do + context 'when nodes have same letter, not terminal and no children' do before do - node_1.letter = :a - node_2.letter = :a + node_one.letter = :a + node_two.letter = :a end it 'returns true' do - expect(node_1).to eq node_2 + expect(node_one).to eq node_two end end context 'when the nodes have the same letter and are terminal' do before do - node_1.letter = :a - node_1.terminal! + node_one.letter = :a + node_one.terminal! - node_2.letter = :a - node_2.terminal! + node_two.letter = :a + node_two.terminal! end it 'returns true' do - expect(node_1).to eq node_2 - end - end - - context 'when the nodes have the same letter and are not terminal' do - before do - node_1.letter = :a - node_2.letter = :a - end - - it 'returns true' do - expect(node_1).to eq node_2 + expect(node_one).to eq node_two end end context 'when the nodes have the same letter but are not both terminal' do before do - node_1.letter = :a - node_1.terminal! + node_one.letter = :a + node_one.terminal! - node_2.letter = :a + node_two.letter = :a end + it 'returns false' do - expect(node_1).not_to eq node_2 + expect(node_one).not_to eq node_two end end context 'when the nodes have the same letter and the same children' do before do - node_1.letter = :t - add_words node_1, %w(hese hree hings) + node_one.letter = :t + add_words node_one, %w(hese hree hings) - node_2.letter = :t - add_words node_2, %w(hese hree hings) + node_two.letter = :t + add_words node_two, %w(hese hree hings) end it 'returns true' do - expect(node_1).to eq node_2 + expect(node_one).to eq node_two end end context 'when the nodes have the same letter but different children' do before do - node_1.letter = :t - add_words node_1, %w(hese wo) + node_one.letter = :t + add_words node_one, %w(hese wo) - node_2.letter = :t - add_words node_2, %w(hese hree hings) + node_two.letter = :t + add_words node_two, %w(hese hree hings) end it 'returns false' do - expect(node_1).not_to eq node_2 + expect(node_one).not_to eq node_two end end end diff --git a/spec/lib/rambling/trie/compressor_spec.rb b/spec/lib/rambling/trie/compressor_spec.rb index a8b213fa..79e7dbd5 100644 --- a/spec/lib/rambling/trie/compressor_spec.rb +++ b/spec/lib/rambling/trie/compressor_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Rambling::Trie::Compressor do - let(:compressor) { Rambling::Trie::Compressor.new } + let(:compressor) { described_class.new } describe '#compress' do let(:node) { Rambling::Trie::Nodes::Raw.new } @@ -16,9 +16,7 @@ end context 'with at least one word' do - before do - add_words node, %w(all the words) - end + before { add_words node, %w(all the words) } it 'keeps the node letter nil' do compressed = compressor.compress node @@ -28,25 +26,25 @@ end context 'with a single word' do - before do - add_word node, 'all' - end + before { add_word node, 'all' } + # rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations it 'compresses into a single node without children' do compressed = compressor.compress node + compressed_node_a = compressed[:a] - expect(compressed[:a].letter).to eq :all - expect(compressed[:a].children.size).to eq 0 - expect(compressed[:a]).to be_terminal - expect(compressed[:a]).to be_compressed + expect(compressed_node_a.letter).to eq :all + expect(compressed_node_a.children.size).to eq 0 + expect(compressed_node_a).to be_terminal + expect(compressed_node_a).to be_compressed end + # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations end context 'with two words' do - before do - add_words node, %w(all ask) - end + before { add_words node, %w(all ask) } + # rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations it 'compresses into corresponding three nodes' do compressed = compressor.compress node @@ -65,8 +63,10 @@ expect(compressed[:a][:l]).to be_compressed expect(compressed[:a][:s]).to be_compressed end + # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations end + # rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations it 'reassigns the parent nodes correctly' do add_words node, %w(repay rest repaint) compressed = compressor.compress node @@ -91,7 +91,9 @@ expect(compressed[:r][:p][:i].parent).to eq compressed[:r][:p] expect(compressed[:r][:p][:i].children.size).to eq 0 end + # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations + # rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations it 'does not compress terminal nodes' do add_words node, %w(you your yours) compressed = compressor.compress node @@ -104,5 +106,6 @@ expect(compressed[:y][:r][:s].letter).to eq :s expect(compressed[:y][:r][:s]).to be_compressed end + # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations end end diff --git a/spec/lib/rambling/trie/configuration/properties_spec.rb b/spec/lib/rambling/trie/configuration/properties_spec.rb index 36dddccd..ace496c1 100644 --- a/spec/lib/rambling/trie/configuration/properties_spec.rb +++ b/spec/lib/rambling/trie/configuration/properties_spec.rb @@ -3,13 +3,17 @@ require 'spec_helper' describe Rambling::Trie::Configuration::Properties do - let(:properties) { Rambling::Trie::Configuration::Properties.new } + let(:properties) { described_class.new } describe '.new' do - it 'configures the serializers' do + it 'configures the serializer formats' do serializers = properties.serializers - expect(serializers.formats).to match_array %i(marshal yaml yml zip) + end + + # rubocop:disable RSpec/ExampleLength + it 'configures the serializer providers' do + serializers = properties.serializers expect(serializers.providers.to_a).to match_array [ [:marshal, Rambling::Trie::Serializers::Marshal], [:yaml, Rambling::Trie::Serializers::Yaml], @@ -17,11 +21,15 @@ [:zip, Rambling::Trie::Serializers::Zip], ] end + # rubocop:enable RSpec/ExampleLength - it 'configures the readers' do + it 'configures the reader formats' do readers = properties.readers - expect(readers.formats).to match_array %i(txt) + end + + it 'configures the reader providers' do + readers = properties.readers expect(readers.providers.to_a).to match_array [ [:txt, Rambling::Trie::Readers::PlainText], ] @@ -44,14 +52,24 @@ properties.readers.add :test, 'test' end - it 'resets the serializers and readers to initial values' do + # rubocop:disable RSpec/MultipleExpectations + it 'resets the serializers to initial values' do expect(properties.serializers.formats).to include :test - expect(properties.readers.formats).to include :test properties.reset expect(properties.serializers.formats).not_to include :test + end + # rubocop:enable RSpec/MultipleExpectations + + # rubocop:disable RSpec/MultipleExpectations + it 'resets the readers to initial values' do + expect(properties.readers.formats).to include :test + + properties.reset + expect(properties.readers.formats).not_to include :test end + # rubocop:enable RSpec/MultipleExpectations end end diff --git a/spec/lib/rambling/trie/configuration/provider_collection_spec.rb b/spec/lib/rambling/trie/configuration/provider_collection_spec.rb index 86f5c1fe..cc1d6e6b 100644 --- a/spec/lib/rambling/trie/configuration/provider_collection_spec.rb +++ b/spec/lib/rambling/trie/configuration/provider_collection_spec.rb @@ -4,15 +4,19 @@ describe Rambling::Trie::Configuration::ProviderCollection do let(:configured_default) { nil } - let(:configured_providers) do + let :configured_providers do { one: first_provider, two: second_provider } end - let(:first_provider) { double :first_provider } - let(:second_provider) { double :second_provider } + let :first_provider do + instance_double 'Rambling::Trie::Serializers::Marshal', :first_provider + end + let :second_provider do + instance_double 'Rambling::Trie::Serializers::Marshal', :second_provider + end - let(:provider_collection) do - Rambling::Trie::Configuration::ProviderCollection.new( + let :provider_collection do + described_class.new( :provider, configured_providers, configured_default, @@ -46,25 +50,31 @@ let(:providers) { provider_collection.providers } before do - allow(providers) .to receive_messages( + allow(providers).to receive_messages( :[] => 'value', keys: %i(a b), ) end + # rubocop:disable RSpec/MultipleExpectations it 'delegates `#[]` to providers' do expect(provider_collection[:key]).to eq 'value' expect(providers).to have_received(:[]).with :key end + # rubocop:enable RSpec/MultipleExpectations + # rubocop:disable RSpec/MultipleExpectations it 'aliases `#formats` to `providers#keys`' do expect(provider_collection.formats).to eq %i(a b) expect(providers).to have_received :keys end + # rubocop:enable RSpec/MultipleExpectations end describe '#add' do - let(:provider) { double :provider } + let :provider do + instance_double 'Rambling::Trie::Serializers::Marshal', :provider + end before do provider_collection.add :three, provider @@ -76,7 +86,9 @@ end describe '#default=' do - let(:other_provider) { double :other_provider } + let :other_provider do + instance_double 'Rambling::Trie::Serializers::Marshal', :other_provider + end context 'when the given value is in the providers list' do it 'changes the default provider' do @@ -106,43 +118,59 @@ expect(provider_collection.default).to be_nil end + # rubocop:disable RSpec/MultipleExpectations it 'raises an ArgumentError for any other provider' do expect do provider_collection.default = other_provider end.to raise_error ArgumentError expect(provider_collection.default).to be_nil end + # rubocop:enable RSpec/MultipleExpectations end end describe '#resolve' do context 'when the file extension is one of the providers' do - it 'returns the corresponding provider' do - expect(provider_collection.resolve 'hola.one').to eq first_provider - expect(provider_collection.resolve 'hola.two').to eq second_provider + [ + ['hola.one', :first_provider], + ['hola.two', :second_provider], + ].each do |test_params| + filepath, provider = test_params + + it 'returns the corresponding provider' do + provider_instance = public_send provider + expect(provider_collection.resolve filepath).to eq provider_instance + end end end context 'when the file extension is not one of the providers' do - it 'returns the default provider' do - expect(provider_collection.resolve 'hola.unknown').to eq first_provider - expect(provider_collection.resolve 'hola').to eq first_provider + %w(hola.unknown hola).each do |filepath| + it 'returns the default provider' do + expect(provider_collection.resolve filepath).to eq first_provider + end end end end describe '#reset' do let(:configured_default) { second_provider } - let(:provider) { double :provider } + let :provider do + instance_double 'Rambling::Trie::Serializers::Marshal', :provider + end before do provider_collection.add :three, provider provider_collection.default = provider end - it 'resets to back to the initially configured values' do + it 'resets to back to the initially configured values (:three => nil)' do provider_collection.reset expect(provider_collection[:three]).to be_nil + end + + it 'resets to back to the initially configured default' do + provider_collection.reset expect(provider_collection.default).to eq second_provider end end diff --git a/spec/lib/rambling/trie/container_spec.rb b/spec/lib/rambling/trie/container_spec.rb index b976dea8..71677268 100644 --- a/spec/lib/rambling/trie/container_spec.rb +++ b/spec/lib/rambling/trie/container_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe Rambling::Trie::Container do - let(:container) { Rambling::Trie::Container.new root, compressor } + subject(:container) { described_class.new root, compressor } + let(:compressor) { Rambling::Trie::Compressor.new } let(:root) { Rambling::Trie::Nodes::Raw.new } @@ -16,7 +17,7 @@ it 'yields the container' do yielded = nil - container = Rambling::Trie::Container.new root, compressor do |c| + container = described_class.new root, compressor do |c| yielded = c end @@ -26,15 +27,18 @@ end describe '#add' do + # rubocop:disable RSpec/MultipleExpectations it 'adds the word to the root node' do add_word container, 'hello' expect(root.children.size).to eq 1 expect(root.to_a).to eq %w(hello) end + # rubocop:enable RSpec/MultipleExpectations end describe '#concat' do + # rubocop:disable RSpec/MultipleExpectations it 'adds all the words to the root node' do container.concat %w(other words) @@ -48,42 +52,7 @@ expect(nodes.first.letter).to eq :o expect(nodes.last.letter).to eq :w end - end - - describe '#compress!' do - let(:node) { Rambling::Trie::Nodes::Compressed.new } - - before do - allow(compressor).to receive(:compress).and_return node - - add_word root, 'yes' - node[:yes] = Rambling::Trie::Nodes::Compressed.new - end - - it 'compresses the trie using the compressor' do - container.compress! - - expect(compressor).to have_received(:compress).with root - end - - it 'changes to the root returned by the compressor' do - container.compress! - - expect(container.root).not_to eq root - expect(container.root).to eq node - end - - it 'returns itself' do - expect(container.compress!).to eq container - end - - it 'does not compress multiple times' do - container.compress! - allow(node).to receive(:compressed?).and_return(true) - - container.compress! - expect(compressor).to have_received(:compress).once - end + # rubocop:enable RSpec/MultipleExpectations end describe '#compress' do @@ -102,12 +71,14 @@ expect(compressor).to have_received(:compress).with root end + # rubocop:disable RSpec/MultipleExpectations it 'returns a container with the new root' do new_container = container.compress expect(new_container.root).not_to eq root expect(new_container.root).to eq node end + # rubocop:enable RSpec/MultipleExpectations it 'returns a new container' do expect(container.compress).not_to eq container @@ -128,153 +99,53 @@ end end - describe '#word?' do - let(:root) do - double :root, - compressed?: compressed, - word?: nil - end + describe '#compress!' do + let(:node) { Rambling::Trie::Nodes::Compressed.new } - context 'for an uncompressed root' do - let(:compressed) { true } + context 'with a mocked result' do + before do + allow(compressor).to receive(:compress).and_return node - it 'calls the root with the word characters' do - container.word? 'words' - expect(root).to have_received(:word?).with %w(w o r d s) + add_word root, 'yes' + node[:yes] = Rambling::Trie::Nodes::Compressed.new end - end - context 'for a compressed root' do - let(:compressed) { false } + it 'compresses the trie using the compressor' do + container.compress! - it 'calls the root with the full word' do - container.word? 'words' - expect(root).to have_received(:word?).with %w(w o r d s) + expect(compressor).to have_received(:compress).with root end - end - end - describe '#partial_word?' do - let(:root) do - double :root, - compressed?: compressed, - partial_word?: nil - end - - context 'for an uncompressed root' do - let(:compressed) { true } + # rubocop:disable RSpec/MultipleExpectations + it 'changes to the root returned by the compressor' do + container.compress! - it 'calls the root with the word characters' do - container.partial_word? 'words' - expect(root).to have_received(:partial_word?).with %w(w o r d s) + expect(container.root).not_to eq root + expect(container.root).to eq node end - end - - context 'for a compressed root' do - let(:compressed) { false } + # rubocop:enable RSpec/MultipleExpectations - it 'calls the root with the word characters' do - container.partial_word? 'words' - expect(root).to have_received(:partial_word?).with %w(w o r d s) + it 'returns itself' do + expect(container.compress!).to eq container end - end - end - - describe 'delegates and aliases' do - before do - allow(root).to receive_messages( - :[] => nil, - add: nil, - as_word: nil, - children: nil, - children_tree: nil, - compressed?: nil, - each: nil, - key?: nil, - inspect: nil, - letter: nil, - parent: nil, - partial_word?: nil, - scan: nil, - size: nil, - to_s: nil, - word?: nil, - ) - end - it 'aliases `#include?` to `#word?`' do - container.include? 'words' - expect(root).to have_received(:word?).with %w(w o r d s) - end - - it 'aliases `#match?` to `#partial_word?`' do - container.match? 'words' - expect(root).to have_received(:partial_word?).with %w(w o r d s) - end - - it 'aliases `#words` to `#scan`' do - container.words 'hig' - expect(root).to have_received(:scan).with %w(h i g) - end - - it 'aliases `#<<` to `#add`' do - container << 'words' - expect(root).to have_received(:add).with %i(s d r o w) - end - - it 'delegates `#[]` to the root node' do - container[:yep] - expect(root).to have_received(:[]).with :yep - end - - it 'delegates `#children` to the root node' do - container.children - expect(root).to have_received :children - end - - it 'delegates `#children_tree` to the root node' do - container.children_tree - expect(root).to have_received :children_tree - end - - it 'delegates `#compressed?` to the root node' do - container.compressed? - expect(root).to have_received :compressed? - end - - it 'delegates `#key?` to the root node' do - container.key? :yup - expect(root).to have_received(:key?).with :yup - end - - it 'aliases `#has_key?` to `#key?`' do - container.has_key? :yup - expect(root).to have_received(:key?).with :yup - end - - it 'aliases `#has_letter?` to `#has_key?`' do - container.has_letter? :yup - expect(root).to have_received(:key?).with :yup - end - - it 'delegates `#inspect` to the root node' do - container.inspect - expect(root).to have_received :inspect - end + it 'does not compress multiple times' do + container.compress! + allow(node).to receive(:compressed?).and_return(true) - it 'delegates `#size` to the root node' do - container.size - expect(root).to have_received :size - end - end + container.compress! + expect(compressor).to have_received(:compress).once + end - describe '#compress!' do - it 'gets a new root from the compressor' do - container.compress! + # rubocop:disable RSpec/MultipleExpectations + it 'gets a new root from the compressor' do + container.compress! - expect(container.root).not_to be root - expect(container.root).to be_compressed - expect(root).not_to be_compressed + expect(container.root).not_to be root + expect(container.root).to be_compressed + expect(root).not_to be_compressed + end + # rubocop:enable RSpec/MultipleExpectations end it 'generates a new root with the words from the passed root' do @@ -283,9 +154,7 @@ add_words container, words container.compress! - words.each do |word| - expect(container).to include word - end + words.each { |word| expect(container).to include word } end describe 'and trying to add a word' do @@ -301,138 +170,91 @@ end describe '#word?' do - context 'word is contained' do - before do - add_words container, %w(hello high) - end + it_behaves_like 'a propagating node' do + let(:method_name) { :word? } + end - it 'matches the whole word' do - expect(container.word? 'hello').to be true - expect(container.word? 'high').to be true - end + context 'when word is contained' do + before { add_words container, %w(hello high) } - context 'and the root has been compressed' do - before do - container.compress! - end + it_behaves_like 'a matching container#word' - it 'matches the whole word' do - expect(container.word? 'hello').to be true - expect(container.word? 'high').to be true - end + context 'with compressed root' do + before { container.compress! } + + it_behaves_like 'a matching container#word' end end - context 'word is not contained' do - before do - add_word container, 'hello' - end + context 'when word is not contained' do + before { add_word container, 'hello' } - it 'does not match the whole word' do - expect(container.word? 'halt').to be false - expect(container.word? 'al').to be false - end + it_behaves_like 'a non-matching container#word' - context 'and the root has been compressed' do - before do - container.compress! - end + context 'with compressed root' do + before { container.compress! } - it 'does not match the whole word' do - expect(container.word? 'halt').to be false - expect(container.word? 'al').to be false - end + it_behaves_like 'a non-matching container#word' end end end describe '#partial_word?' do - context 'word is contained' do - before do - add_words container, %w(hello high) + context 'with underlying node' do + it_behaves_like 'a propagating node' do + let(:method_name) { :partial_word? } end + end - it 'matches part of the word' do - expect(container.partial_word? 'hell').to be true - expect(container.partial_word? 'hig').to be true - end + context 'when word is contained' do + before { add_words container, %w(hello high) } - context 'and the root has been compressed' do - before do - container.compress! - end + it_behaves_like 'a matching container#partial_word' - it 'matches part of the word' do - expect(container.partial_word? 'h').to be true - expect(container.partial_word? 'he').to be true - expect(container.partial_word? 'hell').to be true - expect(container.partial_word? 'hello').to be true - expect(container.partial_word? 'hi').to be true - expect(container.partial_word? 'hig').to be true - expect(container.partial_word? 'high').to be true - end - end - end + context 'with compressed root' do + before { container.compress! } - shared_examples_for 'a non matching tree' do - it 'does not match any part of the word' do - %w(ha hal al).each do |word| - expect(container.partial_word? word).to be false - end + it_behaves_like 'a matching container#partial_word' end end - context 'word is not contained' do - before do - add_word container, 'hello' - end + context 'when word is not contained' do + before { add_word container, 'hello' } - context 'and the root is uncompressed' do - it_behaves_like 'a non matching tree' - end + it_behaves_like 'a non-matching container#partial_word' - context 'and the root has been compressed' do - it_behaves_like 'a non matching tree' + context 'with compressed root' do + before { container.compress! } + + it_behaves_like 'a non-matching container#partial_word' end end end describe '#scan' do - context 'words that match are not contained' do + context 'when words that match are contained' do before do add_words container, %w(hi hello high hell highlight histerical) end - it 'returns an array with the words that match' do - expect(container.scan 'hi').to eq %w(hi high highlight histerical) - expect(container.scan 'hig').to eq %w(high highlight) - end + it_behaves_like 'a matching container#scan' - context 'and the root has been compressed' do - before do - container.compress! - end + context 'with compressed root' do + before { container.compress! } - it 'returns an array with the words that match' do - expect(container.scan 'hi').to eq %w(hi high highlight histerical) - expect(container.scan 'hig').to eq %w(high highlight) - end + it_behaves_like 'a matching container#scan' end end - context 'words that match are not contained' do - before do - add_word container, 'hello' - end + context 'when words that match are not contained' do + before { add_word container, 'hello' } it 'returns an empty array' do expect(container.scan 'hi').to eq %w() end - context 'and the root has been compressed' do - before do - container.compress! - end + context 'with compressed root' do + before { container.compress! } it 'returns an empty array' do expect(container.scan 'hi').to eq %w() @@ -442,19 +264,15 @@ end describe '#words_within' do - before do - add_words container, %w(one word and other words) - end + before { add_words container, %w(one word and other words) } - context 'phrase does not contain any words' do + context 'when phrase does not contain any words' do it 'returns an empty array' do expect(container.words_within 'xyz').to match_array [] end - context 'and the node is compressed' do - before do - container.compress! - end + context 'with compressed node' do + before { container.compress! } it 'returns an empty array' do expect(container.words_within 'xyz').to match_array [] @@ -462,33 +280,23 @@ end end - context 'phrase contains one word at the start of the phrase' do - it 'returns an array with the word found in the phrase' do - expect(container.words_within 'word').to match_array %w(word) - expect(container.words_within 'wordxyz').to match_array %w(word) - end + context 'when phrase contains one word at the start of the phrase' do + it_behaves_like 'a matching container#words_within' - context 'and the node is compressed' do - before do - container.compress! - end + context 'with compressed node' do + before { container.compress! } - it 'returns an array with the word found in the phrase' do - expect(container.words_within 'word').to match_array %w(word) - expect(container.words_within 'wordxyz').to match_array %w(word) - end + it_behaves_like 'a matching container#words_within' end end - context 'phrase contains one word at the end of the phrase' do + context 'when phrase contains one word at the end of the phrase' do it 'returns an array with the word found in the phrase' do expect(container.words_within 'xyz word').to match_array %w(word) end - context 'and the node is compressed' do - before do - container.compress! - end + context 'with compressed node' do + before { container.compress! } it 'returns an array with the word found in the phrase' do expect(container.words_within 'xyz word').to match_array %w(word) @@ -496,48 +304,33 @@ end end - context 'phrase contains a few words' do - it 'returns an array with all words found in the phrase' do - expect(container.words_within 'xyzword otherzxyone') - .to match_array %w(word other one) - end + context 'when phrase contains a few words' do + it_behaves_like 'a non-matching container#words_within' - context 'and the node is compressed' do - before do - container.compress! - end + context 'with compressed node' do + before { container.compress! } - it 'returns an array with all words found in the phrase' do - expect(container.words_within 'xyzword otherzxyone') - .to match_array %w(word other one) - end + it_behaves_like 'a non-matching container#words_within' end end end describe '#words_within?' do - before do - add_words container, %w(one word and other words) - end + before { add_words container, %w(one word and other words) } - context 'phrase does not contain any words' do - it 'returns false' do - expect(container.words_within? 'xyz').to be false - end - end + it_behaves_like 'a matching container#words_within?' - context 'phrase contains any word' do - it 'returns true' do - expect(container.words_within? 'xyz words').to be true - expect(container.words_within? 'xyzone word').to be true - end + context 'with compressed node' do + before { container.compress! } + + it_behaves_like 'a non-matching container#words_within?' end end describe '#==' do context 'when the root nodes are the same' do - let(:other_container) do - Rambling::Trie::Container.new container.root, compressor + let :other_container do + described_class.new container.root, compressor end it 'returns true' do @@ -547,13 +340,11 @@ context 'when the root nodes are not the same' do let(:other_root) { Rambling::Trie::Nodes::Raw.new } - let(:other_container) do - Rambling::Trie::Container.new other_root, compressor + let :other_container do + described_class.new other_root, compressor end - before do - add_word other_container, 'hola' - end + before { add_word other_container, 'hola' } it 'returns false' do expect(container).not_to eq other_container @@ -562,9 +353,7 @@ end describe '#each' do - before do - add_words container, %w(yes no why) - end + before { add_words container, %w(yes no why) } it 'returns an enumerator when no block is given' do expect(container.each).to be_instance_of Enumerator @@ -576,9 +365,7 @@ end describe '#inspect' do - before do - add_words container, %w(a few words hello hell) - end + before { add_words container, %w(a few words hello hell) } it 'returns the container class name plus the root inspection' do expect(container.inspect).to eq one_line <<~CONTAINER @@ -588,4 +375,92 @@ CONTAINER end end + + describe 'delegates and aliases' do + before do + allow(root).to receive_messages( + :[] => nil, + add: nil, + as_word: nil, + children: nil, + children_tree: nil, + compressed?: nil, + each: nil, + key?: nil, + inspect: nil, + letter: nil, + parent: nil, + partial_word?: nil, + scan: nil, + size: nil, + to_s: nil, + word?: nil, + ) + end + + it 'aliases `#include?` to `#word?`' do + container.include? 'words' + expect(root).to have_received(:word?).with %w(w o r d s) + end + + it 'aliases `#match?` to `#partial_word?`' do + container.match? 'words' + expect(root).to have_received(:partial_word?).with %w(w o r d s) + end + + it 'aliases `#words` to `#scan`' do + container.words 'hig' + expect(root).to have_received(:scan).with %w(h i g) + end + + it 'aliases `#<<` to `#add`' do + container << 'words' + expect(root).to have_received(:add).with %i(s d r o w) + end + + it 'delegates `#[]` to the root node' do + container[:yep] + expect(root).to have_received(:[]).with :yep + end + + it 'delegates `#children` to the root node' do + container.children + expect(root).to have_received :children + end + + it 'delegates `#children_tree` to the root node' do + container.children_tree + expect(root).to have_received :children_tree + end + + it 'delegates `#compressed?` to the root node' do + container.compressed? + expect(root).to have_received :compressed? + end + + it 'delegates `#key?` to the root node' do + container.key? :yup + expect(root).to have_received(:key?).with :yup + end + + it 'aliases `#has_key?` to `#key?`' do + container.has_key? :yup + expect(root).to have_received(:key?).with :yup + end + + it 'aliases `#has_letter?` to `#has_key?`' do + container.has_letter? :yup + expect(root).to have_received(:key?).with :yup + end + + it 'delegates `#inspect` to the root node' do + container.inspect + expect(root).to have_received :inspect + end + + it 'delegates `#size` to the root node' do + container.size + expect(root).to have_received :size + end + end end diff --git a/spec/lib/rambling/trie/enumerable_spec.rb b/spec/lib/rambling/trie/enumerable_spec.rb index ce338e27..5f67fbac 100644 --- a/spec/lib/rambling/trie/enumerable_spec.rb +++ b/spec/lib/rambling/trie/enumerable_spec.rb @@ -8,21 +8,23 @@ module Trie let(:node) { Rambling::Trie::Nodes::Raw.new } let(:words) { %w(add some words and another word) } - before do - add_words node, words - end + before { add_words node, words } describe '#each' do - it 'returns an enumerator' do - expect(node.each).to be_a Enumerator + it 'returns an enumerator when no block is given' do + expect(node.each).to be_an Enumerator + end + + it 'has the same word count as the trie' do + expect(node.count).to eq words.count end it 'includes every word contained in the trie' do - node.each do |word| - expect(words).to include word - end + node.each { |word| expect(words).to include word } + end - expect(node.count).to eq words.count + it 'returns the enumerable when a block is given' do + expect(node.each { |_| }).to eq node end end @@ -32,9 +34,15 @@ module Trie end end - it 'includes the core Enumerable module' do + it 'includes #all? from the core Enumerable module' do expect(node.all? { |word| words.include? word }).to be true + end + + it 'includes #any? from the core Enumerable module' do expect(node.any? { |word| word.start_with? 's' }).to be true + end + + it 'includes #to_a from the core Enumerable module' do expect(node.to_a).to match_array words end end diff --git a/spec/lib/rambling/trie/inspectable_spec.rb b/spec/lib/rambling/trie/inspectable_spec.rb index fd5f0300..b380b163 100644 --- a/spec/lib/rambling/trie/inspectable_spec.rb +++ b/spec/lib/rambling/trie/inspectable_spec.rb @@ -13,19 +13,23 @@ let(:child) { node[:o] } let(:terminal_node) { node[:o][:n][:l][:y] } - it 'returns a pretty printed version of the node' do + it 'returns a pretty printed version of the parent node' do expect(node.inspect).to eq one_line <<~RAW # RAW + end + it 'returns a pretty printed version of the child node' do expect(child.inspect).to eq one_line <<~CHILD # CHILD + end + it 'returns a pretty printed version of the terminal node' do expect(terminal_node.inspect).to eq one_line <<~TERMINAL # COMPRESSED + end + it 'returns a pretty printed version of the compressed child node' do expect(compressed_child.inspect).to eq one_line <<~CHILD #(content) { content } } end end diff --git a/spec/lib/rambling/trie/serializers/marshal_spec.rb b/spec/lib/rambling/trie/serializers/marshal_spec.rb index 365db5cb..c8fcaf6f 100644 --- a/spec/lib/rambling/trie/serializers/marshal_spec.rb +++ b/spec/lib/rambling/trie/serializers/marshal_spec.rb @@ -4,9 +4,7 @@ describe Rambling::Trie::Serializers::Marshal do it_behaves_like 'a serializer' do - let(:serializer) { Rambling::Trie::Serializers::Marshal.new } - let(:format) { :marshal } - - let(:formatted_content) { Marshal.dump content } + let(:file_format) { :marshal } + let(:format_content) { Marshal.method(:dump) } end end diff --git a/spec/lib/rambling/trie/serializers/serializer_spec.rb b/spec/lib/rambling/trie/serializers/serializer_spec.rb new file mode 100644 index 00000000..3d547dd0 --- /dev/null +++ b/spec/lib/rambling/trie/serializers/serializer_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Rambling::Trie::Serializers::Serializer do + subject(:serializer) { described_class.new } + + describe '#load' do + it 'is an abstract method that raises NotImplementedError' do + expect { serializer.load('any-file.zip') } + .to raise_exception NotImplementedError + end + end + + describe '#dump' do + it 'is an abstract method that raises NotImplementedError' do + expect { serializer.dump('any contents', 'any-file.zip') } + .to raise_exception NotImplementedError + end + end +end diff --git a/spec/lib/rambling/trie/serializers/yaml_spec.rb b/spec/lib/rambling/trie/serializers/yaml_spec.rb index 372803f1..0cedcb17 100644 --- a/spec/lib/rambling/trie/serializers/yaml_spec.rb +++ b/spec/lib/rambling/trie/serializers/yaml_spec.rb @@ -4,9 +4,7 @@ describe Rambling::Trie::Serializers::Yaml do it_behaves_like 'a serializer' do - let(:serializer) { Rambling::Trie::Serializers::Yaml.new } - let(:format) { :yml } - - let(:formatted_content) { YAML.dump content } + let(:file_format) { :yml } + let(:format_content) { YAML.method(:dump) } end end diff --git a/spec/lib/rambling/trie/serializers/zip_spec.rb b/spec/lib/rambling/trie/serializers/zip_spec.rb index 7f06250d..71cc915e 100644 --- a/spec/lib/rambling/trie/serializers/zip_spec.rb +++ b/spec/lib/rambling/trie/serializers/zip_spec.rb @@ -3,26 +3,34 @@ require 'spec_helper' describe Rambling::Trie::Serializers::Zip do - it_behaves_like 'a serializer' do - let(:properties) { Rambling::Trie::Configuration::Properties.new } - let(:serializer) { Rambling::Trie::Serializers::Zip.new properties } - let(:format) { 'marshal.zip' } + { + yaml: YAML.method(:dump), + yml: YAML.method(:dump), + marshal: Marshal.method(:dump), + file: Marshal.method(:dump), + }.each do |file_format, dump_method| + context "with '.#{file_format}'" do + it_behaves_like 'a serializer' do + let(:properties) { Rambling::Trie::Configuration::Properties.new } + let(:serializer) { described_class.new properties } + let(:file_format) { :zip } - before do - properties.tmp_path = tmp_path - end - - let(:filename) { File.basename(filepath).gsub %r{\.zip}, '' } - let(:formatted_content) { zip Marshal.dump content } + let(:filepath) { File.join tmp_path, "trie-root.#{file_format}.zip" } + let(:format_content) { ->(content) { zip dump_method.call content } } + let(:filename) { File.basename(filepath).gsub %r{\.zip}, '' } - def zip content - cursor = Zip::OutputStream.write_buffer do |io| - io.put_next_entry filename - io.write content + before { properties.tmp_path = tmp_path } end + end + end - cursor.rewind - cursor.read + def zip content + cursor = Zip::OutputStream.write_buffer do |io| + io.put_next_entry filename + io.write content end + + cursor.rewind + cursor.read end end diff --git a/spec/lib/rambling/trie/stringifyable_spec.rb b/spec/lib/rambling/trie/stringifyable_spec.rb index 27e28e61..d3677e4a 100644 --- a/spec/lib/rambling/trie/stringifyable_spec.rb +++ b/spec/lib/rambling/trie/stringifyable_spec.rb @@ -6,17 +6,15 @@ describe '#as_word' do let(:node) { Rambling::Trie::Nodes::Raw.new } - context 'for an empty node' do - before do - add_word node, '' - end + context 'with an empty node' do + before { add_word node, '' } it 'returns nil' do expect(node.as_word).to be_empty end end - context 'for one letter' do + context 'with one letter' do before do node.letter = :a add_word node, '' @@ -27,7 +25,7 @@ end end - context 'for a small word' do + context 'with a small word' do before do node.letter = :a add_word node, 'll' @@ -43,7 +41,7 @@ end end - context 'for a long word' do + context 'with a long word' do before do node.letter = :b add_word node, 'eautiful' @@ -54,7 +52,7 @@ end end - context 'for a node with nil letter' do + context 'with a node with nil letter' do let(:node) { Rambling::Trie::Nodes::Raw.new nil } it 'returns nil' do @@ -62,7 +60,7 @@ end end - context 'for a compressed node' do + context 'with a compressed node' do let(:compressor) { Rambling::Trie::Compressor.new } let(:compressed_node) { compressor.compress node } @@ -71,12 +69,18 @@ add_words node, %w(m dd) end - it 'returns the words for the terminal nodes' do - expect(compressed_node[:m].as_word).to eq 'am' - expect(compressed_node[:d].as_word).to eq 'add' + [ + [:m, 'am'], + [:d, 'add'], + ].each do |test_params| + key, expected = test_params + + it "returns the words for terminal nodes (#{key} => #{expected})" do + expect(compressed_node[key].as_word).to eq expected + end end - it 'raise an error for non terminal nodes' do + it 'raises an error for non terminal nodes' do expect { compressed_node.as_word } .to raise_error Rambling::Trie::InvalidOperation end diff --git a/spec/lib/rambling/trie_spec.rb b/spec/lib/rambling/trie_spec.rb index 202ea69d..84068672 100644 --- a/spec/lib/rambling/trie_spec.rb +++ b/spec/lib/rambling/trie_spec.rb @@ -15,25 +15,27 @@ end it 'returns a new instance of the trie container' do - expect(Rambling::Trie.create).to eq container + expect(described_class.create).to eq container end context 'with a block' do it 'yields the new container' do yielded = nil - Rambling::Trie.create { |trie| yielded = trie } + described_class.create { |trie| yielded = trie } expect(yielded).to eq container end end context 'with a filepath' do let(:filepath) { 'a test filepath' } - let(:reader) { double :reader } + let :reader do + instance_double 'Rambling::Trie::Readers::PlainText', :reader + end let(:words) { %w(a couple of test words over here) } before do receive_and_yield = receive(:each_word) - words.inject(receive_and_yield) do |yielder, word| + words.inject receive_and_yield do |yielder, word| yielder.and_yield word end @@ -42,7 +44,7 @@ end it 'loads every word' do - Rambling::Trie.create filepath, reader + described_class.create filepath, reader words.each do |word| expect(container).to have_received(:<<).with word @@ -52,17 +54,23 @@ context 'without any reader' do let(:filepath) { 'a test filepath' } - let(:reader) { double :reader, each_word: nil } + let :reader do + instance_double( + 'Rambling::Trie::Readers::PlainText', + :reader, + each_word: nil, + ) + end before do - Rambling::Trie.config do |c| + described_class.config do |c| c.readers.add :default, reader c.readers.default = reader end end it 'defaults to a plain text reader' do - Rambling::Trie.create filepath, nil + described_class.create filepath, nil expect(reader).to have_received(:each_word).with filepath end @@ -70,29 +78,53 @@ end describe '.load' do + let(:filepath) { 'a path to a file' } let(:root) { Rambling::Trie::Nodes::Raw.new } let(:compressor) { Rambling::Trie::Compressor.new } let(:container) { Rambling::Trie::Container.new root, compressor } - let(:serializer) { double :serializer, load: root } - let(:filepath) { 'a path to a file' } + let :serializer do + instance_double( + 'Rambling::True::Serializers::File', + :serializer, + load: root, + ) + end it 'returns a new container with the loaded root node' do - trie = Rambling::Trie.load filepath, serializer + trie = described_class.load filepath, serializer expect(trie).to eq container end it 'uses the serializer to load the root node from the given filepath' do - Rambling::Trie.load filepath, serializer + described_class.load filepath, serializer expect(serializer).to have_received(:load).with filepath end context 'without a serializer' do - let(:marshal_serializer) { double :marshal_serializer, load: nil } - let(:default_serializer) { double :default_serializer, load: nil } - let(:yaml_serializer) { double :yaml_serializer, load: nil } + let :marshal_serializer do + instance_double( + 'Rambling::Trie::Serializers::Marshal', + :marshal_serializer, + load: nil, + ) + end + let :default_serializer do + instance_double( + 'Rambling::Trie::Serializers::File', + :default_serializer, + load: nil, + ) + end + let :yaml_serializer do + instance_double( + 'Rambling::Trie::Serializers::Yaml', + :yaml_serializer, + load: nil, + ) + end before do - Rambling::Trie.config do |c| + described_class.config do |c| c.serializers.add :default, default_serializer c.serializers.add :marshal, marshal_serializer c.serializers.add :yml, yaml_serializer @@ -102,18 +134,21 @@ end end - it 'determines the serializer based on the file extension' do - Rambling::Trie.load 'test.marshal' - expect(marshal_serializer).to have_received(:load).with 'test.marshal' + [ + ['.marshal', :marshal_serializer], + ['.yml', :yaml_serializer], + ['.yaml', :yaml_serializer], + ['', :default_serializer], + ].each do |test_params| + extension, serializer = test_params + filepath = "test#{extension}" - Rambling::Trie.load 'test.yml' - expect(yaml_serializer).to have_received(:load).with 'test.yml' + it "uses extension-based serializer (#{filepath} -> #{serializer})" do + serializer_instance = public_send serializer - Rambling::Trie.load 'test.yaml' - expect(yaml_serializer).to have_received(:load).with 'test.yaml' - - Rambling::Trie.load 'test' - expect(default_serializer).to have_received(:load).with 'test' + described_class.load filepath + expect(serializer_instance).to have_received(:load).with filepath + end end end @@ -121,7 +156,7 @@ it 'yields the new container' do yielded = nil - Rambling::Trie.load filepath, serializer do |trie| + described_class.load filepath, serializer do |trie| yielded = trie end @@ -132,16 +167,36 @@ describe '.dump' do let(:filename) { 'a trie' } - let(:root) { double :root } - let(:compressor) { double :compressor } + let(:root) { instance_double 'Rambling::Trie::Serializers::Marshal', :root } + let :compressor do + instance_double 'Rambling::Trie::Serializers::Marshal', :compressor + end let(:trie) { Rambling::Trie::Container.new root, compressor } - let(:marshal_serializer) { double :marshal_serializer, dump: nil } - let(:yaml_serializer) { double :yaml_serializer, dump: nil } - let(:default_serializer) { double :default_serializer, dump: nil } + let :marshal_serializer do + instance_double( + 'Rambling::Trie::Serializers::Marshal', + :marshal_serializer, + dump: nil, + ) + end + let :yaml_serializer do + instance_double( + 'Rambling::Trie::Serializers::Yaml', + :yaml_serializer, + dump: nil, + ) + end + let :default_serializer do + instance_double( + 'Rambling::Trie::Serializers::File', + :default_serializer, + dump: nil, + ) + end before do - Rambling::Trie.config do |c| + described_class.config do |c| c.serializers.add :default, default_serializer c.serializers.add :marshal, marshal_serializer c.serializers.add :yml, yaml_serializer @@ -151,32 +206,39 @@ end it 'uses the configured default serializer by default' do - Rambling::Trie.dump trie, filename + described_class.dump trie, filename expect(default_serializer).to have_received(:dump).with root, filename end context 'when provided with a format' do - it 'uses the corresponding serializer' do - Rambling::Trie.dump trie, "#{filename}.marshal" - expect(marshal_serializer).to have_received(:dump) - .with root, "#{filename}.marshal" - - Rambling::Trie.dump trie, "#{filename}.yml" - expect(yaml_serializer).to have_received(:dump) - .with root, "#{filename}.yml" + [ + ['.marshal', :marshal_serializer], + ['.yml', :yaml_serializer], + ['', :default_serializer], + ].each do |test_params| + extension, serializer = test_params + + it 'uses the corresponding serializer' do + filepath = "#{filename}#{extension}" + serializer_instance = public_send serializer + + described_class.dump trie, filepath + expect(serializer_instance).to have_received(:dump) + .with root, filepath + end end end end describe '.config' do it 'returns the properties' do - expect(Rambling::Trie.config).to eq Rambling::Trie.send :properties + expect(described_class.config).to eq described_class.send :properties end it 'yields the properties' do yielded = nil - Rambling::Trie.config { |c| yielded = c } - expect(yielded).to eq Rambling::Trie.send :properties + described_class.config { |c| yielded = c } + expect(yielded).to eq described_class.send :properties end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2d033be9..7a4dd7e1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,17 +1,21 @@ # frozen_string_literal: true +require 'yaml' require 'simplecov' -require 'coveralls' -Coveralls.wear! +COVERAGE_FILTER = %r{/spec/}.freeze -SimpleCov.formatters = [ - SimpleCov::Formatter::HTMLFormatter, - Coveralls::SimpleCov::Formatter, -] +if ENV.key? 'COVERALLS_REPO_TOKEN' + require 'coveralls' -SimpleCov.start do - add_filter '/spec/' + SimpleCov.formatters = [ + SimpleCov::Formatter::HTMLFormatter, + Coveralls::SimpleCov::Formatter, + ] + + Coveralls.wear! { add_filter COVERAGE_FILTER } +else + SimpleCov.start { add_filter COVERAGE_FILTER } end require 'rspec' @@ -23,7 +27,7 @@ config.tty = true config.formatter = :documentation config.order = :random - config.run_all_when_everything_filtered = true + config.filter_run_when_matching :focus config.raise_errors_for_deprecations! end @@ -31,7 +35,8 @@ %w( a_compressible_trie a_serializable_trie a_serializer a_trie_data_structure - a_trie_node a_trie_node_implementation + a_trie_node a_trie_node_implementation a_container_scan a_container_word + a_container_partial_word a_container_words_within ).each do |name| require File.join('support', 'shared_examples', name) end diff --git a/spec/support/shared_examples/a_compressible_trie.rb b/spec/support/shared_examples/a_compressible_trie.rb index 02be510b..e8096591 100644 --- a/spec/support/shared_examples/a_compressible_trie.rb +++ b/spec/support/shared_examples/a_compressible_trie.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true shared_examples_for 'a compressible trie' do - context 'and the trie is not compressed' do + context 'with an uncompressed trie' do it_behaves_like 'a trie data structure' it 'does not alter the input' do @@ -16,7 +16,7 @@ end end - context 'and the trie is compressed' do + context 'with an compressed trie' do let!(:original_root) { trie.root } let!(:original_keys) { original_root.children_tree.keys } let!(:original_values) { original_root.children_tree.values } @@ -31,9 +31,15 @@ expect(trie).to be_compressed end - it 'leaves the original root intact' do + it 'leaves the original root keys intact' do expect(original_root.children_tree.keys).to eq original_keys + end + + it 'leaves the original trie keys intact' do expect(trie.children_tree.keys).to eq original_keys + end + + it 'leaves the original trie values intact' do expect(trie.children_tree.values).not_to eq original_values end end diff --git a/spec/support/shared_examples/a_container_partial_word.rb b/spec/support/shared_examples/a_container_partial_word.rb new file mode 100644 index 00000000..ebf0e267 --- /dev/null +++ b/spec/support/shared_examples/a_container_partial_word.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +shared_examples_for 'a matching container#partial_word' do + %w(h he hell hello hi hig high).each do |prefix| + it 'matches part of the word' do + expect(container.partial_word? prefix).to be true + end + end +end + +shared_examples_for 'a non-matching container#partial_word' do + it 'does not match any part of the word' do + %w(ha hal al).each do |word| + expect(container.partial_word? word).to be false + end + end +end diff --git a/spec/support/shared_examples/a_container_scan.rb b/spec/support/shared_examples/a_container_scan.rb new file mode 100644 index 00000000..73a97e56 --- /dev/null +++ b/spec/support/shared_examples/a_container_scan.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +shared_examples_for 'a matching container#scan' do + [ + ['hi', %w(hi high highlight histerical)], + ['hig', %w(high highlight)], + ].each do |test_params| + prefix, expected = test_params + + it "returns an array with the words that match '#{prefix}'" do + expect(container.scan prefix).to eq expected + end + end +end diff --git a/spec/support/shared_examples/a_container_word.rb b/spec/support/shared_examples/a_container_word.rb new file mode 100644 index 00000000..fb33bfc0 --- /dev/null +++ b/spec/support/shared_examples/a_container_word.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +shared_examples_for 'a matching container#word' do + %w(hello high).each do |word| + it 'matches the whole word' do + expect(container.word? word).to be true + end + end +end + +shared_examples_for 'a non-matching container#word' do + %w(halt al).each do |word| + it 'does not match the whole word' do + expect(container.word? word).to be false + end + end +end + +shared_examples_for 'a propagating node' do + [ + [true, 'Rambling::Trie::Nodes::Compressed'], + [false, 'Rambling::Trie::Nodes::Raw'], + ].each do |test_params| + compressed_value, instance_double_class = test_params + + context "when root has compressed=#{compressed_value}" do + let :root do + instance_double( + instance_double_class, + :root, + compressed?: compressed_value, + word?: nil, + partial_word?: nil, + ) + end + + it 'calls the root with the word characters' do + container.public_send method_name, 'words' + expect(root).to have_received(method_name).with %w(w o r d s) + end + end + end +end diff --git a/spec/support/shared_examples/a_container_words_within.rb b/spec/support/shared_examples/a_container_words_within.rb new file mode 100644 index 00000000..6f66a27f --- /dev/null +++ b/spec/support/shared_examples/a_container_words_within.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +shared_examples_for 'a matching container#words_within' do + [ + ['word', %w(word)], + ['wordxyz', %w(word)], + ].each do |test_params| + phrase, expected = test_params + + it "returns an array with the word found in the phrase '#{phrase}'" do + expect(container.words_within phrase).to match_array expected + end + end +end + +shared_examples_for 'a non-matching container#words_within' do + it 'returns an array with all words found in the phrase' do + expect(container.words_within 'xyzword otherzxyone') + .to match_array %w(word other one) + end +end + +shared_examples_for 'a matching container#words_within?' do + context 'when phrase does not contain any words' do + it 'returns false' do + expect(container.words_within? 'xyz').to be false + end + end + + context 'when phrase contains any word' do + ['xyz words', 'xyzone word'].each do |phrase| + it "returns true for '#{phrase}'" do + expect(container.words_within? phrase).to be true + end + end + end +end + +shared_examples_for 'a non-matching container#words_within?' do + it 'returns an array with all words found in the phrase' do + expect(container.words_within 'xyzword otherzxyone') + .to match_array %w(word other one) + end +end diff --git a/spec/support/shared_examples/a_serializable_trie.rb b/spec/support/shared_examples/a_serializable_trie.rb index bc1e3292..838cb3af 100644 --- a/spec/support/shared_examples/a_serializable_trie.rb +++ b/spec/support/shared_examples/a_serializable_trie.rb @@ -2,24 +2,20 @@ shared_examples_for 'a serializable trie' do let(:tmp_path) { File.join ::SPEC_ROOT, 'tmp' } - let(:filepath) { File.join tmp_path, "trie-root.#{format}" } + let(:filepath) { File.join tmp_path, "trie-root.#{file_format}" } - context 'and the trie is not compressed' do - before do - Rambling::Trie.dump trie_to_serialize, filepath - end + context 'with an uncompressed trie' do + before { Rambling::Trie.dump trie_to_serialize, filepath } it_behaves_like 'a compressible trie' do let(:trie) { Rambling::Trie.load filepath } end end - context 'and the trie is compressed' do + context 'with an compressed trie' do let(:trie) { Rambling::Trie.load filepath } - before do - Rambling::Trie.dump trie_to_serialize.compress!, filepath - end + before { Rambling::Trie.dump trie_to_serialize.compress!, filepath } it_behaves_like 'a trie data structure' diff --git a/spec/support/shared_examples/a_serializer.rb b/spec/support/shared_examples/a_serializer.rb index bd19d833..b5029d77 100644 --- a/spec/support/shared_examples/a_serializer.rb +++ b/spec/support/shared_examples/a_serializer.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true shared_examples_for 'a serializer' do + subject(:serializer) { described_class.new } + let(:trie) { Rambling::Trie.create } let(:tmp_path) { File.join ::SPEC_ROOT, 'tmp' } - let(:filepath) { File.join tmp_path, "trie-root.#{format}" } + let(:filepath) { File.join tmp_path, "trie-root.#{file_format}" } let(:content) { trie.root } before do @@ -12,26 +14,47 @@ end describe '#dump' do - before do - serializer.dump content, filepath - end + [true, false].each do |compress_value| + context "with compressed=#{compress_value} trie" do + let(:formatted_content) { format_content.call content } - it 'creates the file with the provided path' do - expect(File.exist? filepath).to be true - end + before { trie.compress! if compress_value } + + it 'returns the size in bytes of the file dumped' do + total_bytes = serializer.dump content, filepath + expect(total_bytes).to be_within(20).of formatted_content.size + end - it 'converts the contents to the appropriate format' do - expect(File.read(filepath).size).to be_within(10).of formatted_content.size + it 'creates the file with the provided path' do + serializer.dump content, filepath + expect(File.exist? filepath).to be true + end + + it 'converts the contents to the appropriate format' do + serializer.dump content, filepath + expect(File.size filepath).to be_within(20).of formatted_content.size + end + end end end describe '#load' do - before do - serializer.dump content, filepath - end + [true, false].each do |compress_value| + context "with compressed=#{compress_value} trie" do + before do + trie.compress! if compress_value + serializer.dump content, filepath + end + + it 'loads the dumped object back into memory' do + expect(serializer.load filepath).to eq content + end - it 'loads the dumped object back into memory' do - expect(serializer.load filepath).to eq content + it "loads a compressed=#{compress_value} object" do + loaded = serializer.load filepath + expect(loaded.compressed?).to be compress_value unless :file == file_format + end + end end end end diff --git a/spec/support/shared_examples/a_trie_data_structure.rb b/spec/support/shared_examples/a_trie_data_structure.rb index 7330d6a7..a43589da 100644 --- a/spec/support/shared_examples/a_trie_data_structure.rb +++ b/spec/support/shared_examples/a_trie_data_structure.rb @@ -2,25 +2,39 @@ shared_examples_for 'a trie data structure' do it 'contains all the words previously provided' do - words.each do |word| - expect(trie).to include word - expect(trie.word? word).to be true - end + words.each { |word| expect(trie).to include word } + end + + it 'returns true for #word? for all words previously provided' do + words.each { |word| expect(trie.word? word).to be true } + end + + it 'matches the full word for all words in file' do + words.each { |word| expect(trie.match? word).to be true } + end + + it 'matches the start of all the words in file' do + words.each { |word| expect(trie.match? word[0..-2]).to be true } end - it 'matches the start of all the words from the file' do + it 'returns true for #partial_word? with full word for all words in file' do + words.each { |word| expect(trie.partial_word? word).to be true } + end + + it 'returns true for #partial_word? with the start of all words in file' do + words.each { |word| expect(trie.partial_word? word[0..-2]).to be true } + end + + it 'extracts words within larger strings' do words.each do |word| - expect(trie.match? word).to be true - expect(trie.match? word[0..-2]).to be true - expect(trie.partial_word? word).to be true - expect(trie.partial_word? word[0..-2]).to be true + phrase = "x#{word}y" + expect(trie.words_within phrase).to include word end end it 'identifies words within larger strings' do words.each do |word| phrase = "x#{word}y" - expect(trie.words_within phrase).to include word expect(trie.words_within? phrase).to be true end end diff --git a/spec/support/shared_examples/a_trie_node.rb b/spec/support/shared_examples/a_trie_node.rb index d6030304..c6a867c5 100644 --- a/spec/support/shared_examples/a_trie_node.rb +++ b/spec/support/shared_examples/a_trie_node.rb @@ -21,7 +21,8 @@ end context 'with a letter and a parent' do - let(:parent) { node.class.new } + let(:parent) { node_class.new } + # noinspection RubyArgCount let(:node_with_parent) { node_class.new :a, parent } it 'does not have any letter' do @@ -40,9 +41,7 @@ describe '#root?' do context 'when the node has a parent' do - before do - node.parent = node - end + before { node.parent = node } it 'returns false' do expect(node).not_to be_root @@ -50,9 +49,7 @@ end context 'when the node does not have a parent' do - before do - node.parent = nil - end + before { node.parent = nil } it 'returns true' do expect(node).to be_root @@ -61,12 +58,13 @@ end describe '#terminal!' do + # rubocop:disable RSpec/MultipleExpectations it 'forces the node to be terminal' do expect(node).not_to be_terminal node.terminal! - expect(node).to be_terminal end + # rubocop:enable RSpec/MultipleExpectations it 'returns the node' do expect(node.terminal!).to eq node @@ -74,8 +72,9 @@ end describe 'delegates and aliases' do - let(:children_tree) do - double( + let :children_tree do + instance_double( + 'Hash', :children_tree, :[] => 'value', :[]= => nil, @@ -84,20 +83,21 @@ ) end - before do - node.children_tree = children_tree - end + before { node.children_tree = children_tree } + # rubocop:disable RSpec/MultipleExpectations it 'delegates `#[]` to its children tree' do expect(node[:key]).to eq 'value' expect(children_tree).to have_received(:[]).with :key end + # rubocop:enable RSpec/MultipleExpectations it 'delegates `#[]=` to its children tree' do node[:key] = 'value' expect(children_tree).to have_received(:[]=).with(:key, 'value') end + # rubocop:disable RSpec/MultipleExpectations it 'delegates `#key?` to its children tree' do allow(children_tree).to receive(:key?) .with(:present_key) @@ -106,18 +106,26 @@ expect(node).to have_key(:present_key) expect(node).not_to have_key(:absent_key) end + # rubocop:enable RSpec/MultipleExpectations + # rubocop:disable RSpec/MultipleExpectations it 'delegates `#delete` to its children tree' do expect(node.delete :key).to be true expect(children_tree).to have_received(:delete).with :key end + # rubocop:enable RSpec/MultipleExpectations + # rubocop:disable RSpec/ExampleLength it 'delegates `#children` to its children tree values' do - children = [double(:child_1), double(:child_2)] + children = [ + instance_double('Rambling::Trie::Nodes::Node', :child_one), + instance_double('Rambling::Trie::Nodes::Node', :child_two), + ] allow(children_tree).to receive(:values).and_return children expect(node.children).to eq children end + # rubocop:enable RSpec/ExampleLength it 'aliases `#has_key?` to `#key?`' do node.has_key? :nope diff --git a/spec/support/shared_examples/a_trie_node_implementation.rb b/spec/support/shared_examples/a_trie_node_implementation.rb index 25d236e8..d24b1410 100644 --- a/spec/support/shared_examples/a_trie_node_implementation.rb +++ b/spec/support/shared_examples/a_trie_node_implementation.rb @@ -12,26 +12,22 @@ context 'when the chars array is not empty' do context 'when the node has a tree that matches the characters' do - before do - add_word_to_tree 'abc' - end + before { add_word_to_tree 'abc' } - it 'returns true' do - expect(node.partial_word? %w(a)).to be true - expect(node.partial_word? %w(a b)).to be true - expect(node.partial_word? %w(a b c)).to be true + [%w(a), %w(a b), %w(a b c)].each do |letters| + it "returns true for '#{letters}'" do + expect(node.partial_word? letters).to be true + end end end context 'when the node has a tree that does not match the characters' do - before do - add_word_to_tree 'cba' - end + before { add_word_to_tree 'cba' } - it 'returns false' do - expect(node.partial_word? %w(a)).to be false - expect(node.partial_word? %w(a b)).to be false - expect(node.partial_word? %w(a b c)).to be false + [%w(a), %w(a b), %w(a b c)].each do |letters| + it "returns false for '#{letters}'" do + expect(node.partial_word? letters).to be false + end end end end @@ -40,9 +36,7 @@ describe '#word?' do context 'when the chars array is empty' do context 'when the node is terminal' do - before do - node.terminal! - end + before { node.terminal! } it 'returns true' do expect(node.word? []).to be true @@ -58,9 +52,7 @@ context 'when the chars array is not empty' do context 'when the node has a tree that matches all the characters' do - before do - add_word_to_tree 'abc' - end + before { add_word_to_tree 'abc' } it 'returns true' do expect(node.word? %w(a b c).map(&:dup)).to be true @@ -68,13 +60,12 @@ end context 'when the node subtree does not match all the characters' do - before do - add_word_to_tree 'abc' - end + before { add_word_to_tree 'abc' } - it 'returns false' do - expect(node.word? %w(a).map(&:dup)).to be false - expect(node.word? %w(a b).map(&:dup)).to be false + [%w(a), %w(a b)].each do |letters| + it "returns false for '#{letters}'" do + expect(node.word? letters.map(&:dup)).to be false + end end end end @@ -88,26 +79,34 @@ end context 'when the chars array is not empty' do - before do - add_words_to_tree %w(cba ccab) - end + before { add_words_to_tree %w(cba ccab) } context 'when the chars are found' do - it 'returns the found child' do - expect(node.scan %w(c)).to match_array %w(cba ccab) - expect(node.scan %w(c b)).to match_array %w(cba) - expect(node.scan %w(c b a)).to match_array %w(cba) + [ + [%w(c), %w(cba ccab)], + [%w(c b), %w(cba)], + [%w(c b a), %w(cba)], + ].each do |test_params| + letters, expected = test_params + + it "returns the corresponding children (#{letters} => #{expected})" do + expect(node.scan letters).to match_array expected + end end end context 'when the chars are not found' do - it 'returns a Nodes::Missing' do - expect(node.scan %w(a)).to be_a Rambling::Trie::Nodes::Missing - expect(node.scan %w(a b)).to be_a Rambling::Trie::Nodes::Missing - expect(node.scan %w(a b c)).to be_a Rambling::Trie::Nodes::Missing - expect(node.scan %w(c a)).to be_a Rambling::Trie::Nodes::Missing - expect(node.scan %w(c c b)).to be_a Rambling::Trie::Nodes::Missing - expect(node.scan %w(c b a d)).to be_a Rambling::Trie::Nodes::Missing + [ + %w(a), + %w(a b), + %w(a b c), + %w(c a), + %w(c c b), + %w(c b a d), + ].each do |letters| + it "returns a Nodes::Missing for '#{letters}'" do + expect(node.scan letters).to be_a Rambling::Trie::Nodes::Missing + end end end end @@ -120,9 +119,7 @@ end context 'when the node is terminal' do - before do - node.terminal! - end + before { node.terminal! } it 'adds itself to the words' do expect(node.match_prefix %w(g n i t e)).to include 'i' diff --git a/tasks/helpers/gc.rb b/tasks/helpers/garbage_collection.rb similarity index 90% rename from tasks/helpers/gc.rb rename to tasks/helpers/garbage_collection.rb index 550402e6..da141a69 100644 --- a/tasks/helpers/gc.rb +++ b/tasks/helpers/garbage_collection.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Helpers - module GC + module GarbageCollection def with_gc_stats name = nil puts "Live objects before #{name} - #{::GC.stat[:heap_live_slots]}" yield diff --git a/tasks/ips.rb b/tasks/ips.rb index 881123d5..5ea1142c 100644 --- a/tasks/ips.rb +++ b/tasks/ips.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'benchmark/ips' - namespace :ips do task :pop_shift_slice do compare_pop_shift_slice @@ -12,7 +10,7 @@ end task :hash_has_key_or_direct do - compare_hash_has_key_with_square_brackets + compare_has_key_with_square_brackets end task :attr_accessor_vs_def_method do @@ -33,6 +31,7 @@ end def compare + require 'benchmark/ips' Benchmark.ips do |bm| yield bm @@ -43,21 +42,14 @@ def compare def compare_pop_shift_slice compare do |bm| a = [] + bm.report('push') { a.push 1 } + bm.report('pop') { a.pop } - bm.report 'push/pop' do - a.push 1 - a.pop - end + bm.report('unshift') { a.unshift 1 } + bm.report('shift') { a.shift } - bm.report 'unshift/shift' do - a.unshift 1 - a.shift - end - - bm.report 'shovel(<<)/slice!(0)' do - a << 1 - a.slice! 0 - end + bm.report('shovel(<<)') { a << 1 } + bm.report('slice!(0)') { a.slice! 0 } end end @@ -83,20 +75,20 @@ def compare_symbols_with_array_hash_keys end end -def compare_hash_has_key_with_square_brackets +def compare_has_key_with_square_brackets compare do |bm| - hash = { 'thing' => 'gniht' } + hash = { thing: 'gniht' } bm.report 'key?' do - hash.key? 'thing' + hash.key? :thing end bm.report 'has_key?' do - hash.has_key? 'thing' + hash.has_key? :thing end bm.report '[]' do - !!hash['thing'] + !!hash[:thing] end end end @@ -144,7 +136,7 @@ class TestDelegate require 'forwardable' extend ::Forwardable - delegate [:[]] => :hash + delegate %i([]) => :hash attr_reader :hash @@ -168,7 +160,7 @@ def delegate methods_to_target extend TestMyForwardable::Forwardable - delegate [:[]] => :hash + delegate %i([]) => :hash attr_reader :hash diff --git a/tasks/performance.rb b/tasks/performance.rb index 59032c79..da5d375f 100644 --- a/tasks/performance.rb +++ b/tasks/performance.rb @@ -10,11 +10,9 @@ dependencies = %i(serialization:regenerate performance:directory) task :performance, arg_names => dependencies do |_, args| - require 'benchmark/ips' - configuration = Performance::Configuration.new task = Performance::Task.new configuration - task.run args + task.run(**args) end namespace :performance do diff --git a/tasks/performance/configuration.rb b/tasks/performance/configuration.rb index 59f9a3e4..c4320470 100644 --- a/tasks/performance/configuration.rb +++ b/tasks/performance/configuration.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true require_relative '../helpers/trie' -require_relative '../helpers/gc' +require_relative '../helpers/garbage_collection' require_relative 'sub_tasks' require_relative 'reporters' module Performance class Configuration - include Helpers::GC + include Helpers::GarbageCollection def get type, method type ||= 'all' method ||= 'all' - if type == 'all' + if 'all' == type get_all_types method - elsif method == 'all' + elsif 'all' == method get_all_methods type else tasks[type][method] @@ -165,7 +165,7 @@ def lookups_partial_word_compressed def lookups_scan_raw lambda do |iterations| - if iterations.nil? || iterations == 200_000 + if iterations.nil? || 200_000 == iterations Performance::SubTasks::Lookups::Scan::Raw.new else Performance::SubTasks::Lookups::Scan::Raw.new( @@ -181,7 +181,7 @@ def lookups_scan_raw def lookups_scan_compressed _ = nil lambda do |iterations| - if iterations.nil? || iterations == 200_000 + if iterations.nil? || 200_000 == iterations Performance::SubTasks::Lookups::Scan::Compressed.new else Performance::SubTasks::Lookups::Scan::Compressed.new( diff --git a/tasks/performance/reporters/benchmark.rb b/tasks/performance/reporters/benchmark.rb index 32872dbf..4d3f1c53 100644 --- a/tasks/performance/reporters/benchmark.rb +++ b/tasks/performance/reporters/benchmark.rb @@ -7,6 +7,7 @@ module Performance module Reporters class Benchmark < Performance::Reporters::Reporter def initialize _ = nil, output = $stdout.dup + super() @output = output end @@ -29,9 +30,7 @@ def measure iterations, param require 'benchmark' measure = ::Benchmark.measure do - iterations.times do - result = yield param - end + iterations.times { result = yield param } end output.puts result.to_s.ljust 10 diff --git a/tasks/performance/reporters/call_tree_profile.rb b/tasks/performance/reporters/call_tree_profile.rb index 0abf1366..b7d4c0c5 100644 --- a/tasks/performance/reporters/call_tree_profile.rb +++ b/tasks/performance/reporters/call_tree_profile.rb @@ -6,6 +6,7 @@ module Performance module Reporters class CallTreeProfile < Performance::Reporters::Reporter def initialize dirname + super() @dirname = dirname end @@ -13,15 +14,11 @@ def do_report iterations, params FileUtils.mkdir_p dirpath require 'ruby-prof' - result = RubyProf.profile merge_fibers: true do - params.each do |param| - iterations.times do - yield param - end - end - end - - printer = RubyProf::CallTreePrinter.new result + profile = RubyProf::Profile.new + profile.profile { params.each { |p| iterations.times { yield p } } } + profile.merge! + + printer = RubyProf::CallTreePrinter.new profile printer.print path: dirpath end diff --git a/tasks/performance/reporters/flamegraph.rb b/tasks/performance/reporters/flamegraph.rb index 69d2db21..9493eac7 100644 --- a/tasks/performance/reporters/flamegraph.rb +++ b/tasks/performance/reporters/flamegraph.rb @@ -6,6 +6,7 @@ module Performance module Reporters class Flamegraph < Performance::Reporters::Reporter def initialize filename + super() @filename = filename end @@ -14,11 +15,7 @@ def do_report iterations, params require 'flamegraph' ::Flamegraph.generate filepath do - params.each do |param| - iterations.times do - yield param - end - end + params.each { |p| iterations.times { yield p } } end end diff --git a/tasks/performance/reporters/memory_profile.rb b/tasks/performance/reporters/memory_profile.rb index cb3543ec..e191f967 100644 --- a/tasks/performance/reporters/memory_profile.rb +++ b/tasks/performance/reporters/memory_profile.rb @@ -6,6 +6,7 @@ module Performance module Reporters class MemoryProfile < Performance::Reporters::Reporter def initialize filename + super() @filename = filename end @@ -18,11 +19,7 @@ def do_report iterations, params ignore_files: 'lib/rambling/trie/tasks', ) do with_gc_stats "performing #{filename}" do - params.each do |param| - iterations.times do - yield param - end - end + params.each { |p| iterations.times { yield p } } end end diff --git a/tasks/performance/reporters/reporter.rb b/tasks/performance/reporters/reporter.rb index fc2c2138..958e26a0 100644 --- a/tasks/performance/reporters/reporter.rb +++ b/tasks/performance/reporters/reporter.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../../helpers/gc' +require_relative '../../helpers/garbage_collection' require_relative '../../helpers/path' require_relative '../../helpers/time' require_relative '../../helpers/trie' @@ -8,17 +8,15 @@ module Performance module Reporters class Reporter - include Helpers::GC + include Helpers::GarbageCollection include Helpers::Path include Helpers::Time - def report iterations = 1, params = nil + def report iterations = 1, params = nil, &block params = Array params params << nil unless params.any? - do_report iterations, params do |param| - yield param - end + do_report iterations, params, &block end end end diff --git a/tasks/performance/sub_tasks/initialization.rb b/tasks/performance/sub_tasks/initialization.rb index fc026942..53ca23ef 100644 --- a/tasks/performance/sub_tasks/initialization.rb +++ b/tasks/performance/sub_tasks/initialization.rb @@ -8,6 +8,7 @@ class Initialization < Performance::SubTasks::SubTask include Helpers::Path def initialize iterations = 5 + super() @iterations = iterations end diff --git a/tasks/performance/sub_tasks/lookups/lookup.rb b/tasks/performance/sub_tasks/lookups/lookup.rb index 84c2177c..9fba5fdb 100644 --- a/tasks/performance/sub_tasks/lookups/lookup.rb +++ b/tasks/performance/sub_tasks/lookups/lookup.rb @@ -9,6 +9,7 @@ class Lookup < Performance::SubTasks::SubTask include Helpers::Trie def initialize iterations = 200_000 + super() @iterations = iterations end diff --git a/tasks/performance/sub_tasks/lookups/scan/compressed.rb b/tasks/performance/sub_tasks/lookups/scan/compressed.rb index 45cfa961..4d238054 100644 --- a/tasks/performance/sub_tasks/lookups/scan/compressed.rb +++ b/tasks/performance/sub_tasks/lookups/scan/compressed.rb @@ -10,6 +10,7 @@ class Compressed < Performance::SubTasks::Lookups::Lookup include Helpers::Trie def initialize params_to_iterations = default_params + super() @params_to_iterations = params_to_iterations end diff --git a/tasks/performance/sub_tasks/lookups/scan/raw.rb b/tasks/performance/sub_tasks/lookups/scan/raw.rb index fdf0cb74..930f2378 100644 --- a/tasks/performance/sub_tasks/lookups/scan/raw.rb +++ b/tasks/performance/sub_tasks/lookups/scan/raw.rb @@ -10,6 +10,7 @@ class Raw < Performance::SubTasks::Lookups::Lookup include Helpers::Trie def initialize params_to_iterations = default_params + super() @params_to_iterations = params_to_iterations end