diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b90d526 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Test + +on: [pull_request] + +# permissions: +# contents: read + +jobs: + lint: + runs-on: ubuntu-latest + # env: + # BUNDLE_ONLY: rubocop + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby 3.2.2 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2.2 + bundler-cache: true + + - name: Run Tests + run: bundle exec rake + # run: bundle exec rubocop --parallel \ No newline at end of file diff --git a/.gitignore b/.gitignore index cb37805..cc01464 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ /spec/reports/ /tmp/ Gemfile.lock -.byebug_history \ No newline at end of file +.rspec_status \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..34c5164 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/.rubocop-rspec.yml b/.rubocop-rspec.yml new file mode 100644 index 0000000..45182e0 --- /dev/null +++ b/.rubocop-rspec.yml @@ -0,0 +1,11 @@ +########################################################### +#################### Rubocop Rspec ######################## +########################################################### + +# You can find all configuration options for rubocop-rspec here: https://docs.rubocop.org/rubocop-rspec/cops.html + +# RSpec/PendingWithoutReason: +# Enabled: false + +# RSpec/FilePath: +# Enabled: false \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..b57774b --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,77 @@ +# see example at https://gist.github.com/jhass/a5ae80d87f18e53e7b56 + +# <% unless ENV['BYPASS_RUBOCOP_TODO'] %> +# inherit_from: +# <% else %> +# inherit_from: +# - '.rubocop-todo.yml' +# <% end %> + +inherit_from: + - .rubocop_todo.yml + - .rubocop-rspec.yml + +require: + - rubocop-rspec + - rubocop-rake + - rubocop-performance + +AllCops: + NewCops: enable + # TargetRubyVersion: 2.7.8 + # TargetRailsVersion: 6.1.4 + # Exclude: + # - 'Gemfile.lock' + +Naming/VariableNumber: + Enabled: false + +Layout/SpaceInsideHashLiteralBraces: + Enabled: false + +Layout/EmptyLinesAroundModuleBody: + EnforcedStyle: empty_lines_special + Enabled: false + +Layout/TrailingEmptyLines: + Enabled: false + EnforcedStyle: final_blank_line + +Layout/EmptyLinesAroundClassBody: + Enabled: false + +Style/RaiseArgs: + EnforcedStyle: compact + +Naming/MethodParameterName: + Enabled: false + +Naming/VariableName: + Enabled: false + +Layout/FirstHashElementIndentation: + Enabled: false + +Layout/CaseIndentation: + EnforcedStyle: end + +Metrics/ParameterLists: + Enabled: false + +Style/Lambda: + EnforcedStyle: literal + +Layout/IndentationWidth: + Enabled: false + +Layout/EndAlignment: + Enabled: false + +Layout/ElseAlignment: + Enabled: false + +Style/TrivialAccessors: + Enabled: false + +Layout/MultilineMethodCallIndentation: + EnforcedStyle: indented \ No newline at end of file diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..032a15d --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,42 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2024-03-20 19:14:10 UTC using RuboCop version 1.62.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 19 + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 115 + +# Offense count: 2 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 15 + +# Offense count: 1 +# Configuration parameters: CountAsOne. +RSpec/ExampleLength: + Max: 7 + +# Offense count: 3 +RSpec/MultipleExpectations: + Max: 2 + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. +# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces +# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object +# FunctionalMethods: let, let!, subject, watch +# AllowedMethods: lambda, proc, it +Style/BlockDelimiters: + Exclude: + - 'spec/string_replacer/replacer_spec.rb' diff --git a/Gemfile b/Gemfile index 87e0c54..95c44a8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,19 @@ -source "https://rubygems.org" +# frozen_string_literal: true -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } +source 'https://rubygems.org' + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in string_replacer.gemspec gemspec + +# gem 'bundler', '2.4.22' +gem 'debug', '>= 1.0.0' +gem 'rake', '~> 13.1' +gem 'rspec', '~> 3.9' + +# rubocop +gem 'rubocop', '~> 1.62' +gem 'rubocop-performance', '~> 1.20' +gem 'rubocop-rake', '~> 0.6' +gem 'rubocop-rspec', '~> 2.27.1' \ No newline at end of file diff --git a/Rakefile b/Rakefile index 43022f7..f3ca974 100644 --- a/Rakefile +++ b/Rakefile @@ -1,2 +1,11 @@ -require "bundler/gem_tasks" -task :default => :spec +# frozen_string_literal: true + +require 'bundler/gem_tasks' + +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) + +require 'rubocop/rake_task' +RuboCop::RakeTask.new + +task default: %i[spec rubocop] \ No newline at end of file diff --git a/bin/console b/bin/console index 5752631..990c112 100755 --- a/bin/console +++ b/bin/console @@ -1,7 +1,8 @@ #!/usr/bin/env ruby +# frozen_string_literal: true -require "bundler/setup" -require "string_replacer" +require 'bundler/setup' +require 'string_replacer' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. @@ -10,5 +11,5 @@ require "string_replacer" # require "pry" # Pry.start -require "irb" +require 'irb' IRB.start(__FILE__) diff --git a/lib/string_replacer.rb b/lib/string_replacer.rb index b000ed0..7479dc6 100644 --- a/lib/string_replacer.rb +++ b/lib/string_replacer.rb @@ -1,4 +1,6 @@ -require "string_replacer/version" +# frozen_string_literal: true + +require 'string_replacer/version' module StringReplacer # @@ -7,40 +9,45 @@ module StringReplacer # class Replacer - #initialize class instance variable - instance_variable_set('@registered_helpers', []) + # initialize class instance variable + instance_variable_set(:@registered_helpers, []) - #class methods + # class methods class << self attr_accessor :registered_helpers def inherited(subclass) - #support subclassing - subclass.instance_variable_set('@registered_helpers', registered_helpers) + # support subclassing + subclass.instance_variable_set(:@registered_helpers, registered_helpers) super end # Registers a helper to allow its usage. # # @return [String] name of registered helper - def register_helper(name, &block) + def register_helper(name, &) name = name.to_sym - define_method(name, &block) #or define_singleton_method? + define_method(name, &) # or define_singleton_method? @registered_helpers.push(name) name end - + # Unregisters a helper from class # # @return [Array] Remaining helpers def unregister_helper(name) name = name.to_sym - undef_method name rescue false - @registered_helpers = @registered_helpers - [name] + + begin + undef_method name + @registered_helpers -= [name] + rescue NoMethodError + # noop + end end end - + # IMPORTANT! always keep in sync method name regex! [a-zA-Z0-9_]* # Regex used to search for handlebars and its most important features # by using the following named captures. @@ -52,7 +59,7 @@ def unregister_helper(name) # # @example # " {{capitalize(swapcase(hey))}} {{swapcase(user_name())}}".scan(INNERMOST_HELPER_REGEX) - # + # # [ # [ # "{{capitalize(swapcase(hey))}}", 1) handlebars @@ -65,7 +72,7 @@ def unregister_helper(name) # [...] # ] HELPER_REGEX = /[a-zA-Z0-9_-]/ - INNERMOST_HELPER_REGEX = / + INNERMOST_HELPER_REGEX = %r{ (? {{ # opening double braces (? # open none capture group everything before innermost helper @@ -81,7 +88,7 @@ def unregister_helper(name) ) \( # parenthesis (? - [a-zA-Z0-9_.,'" \\|*\/+-]* # innermost helper arguments + [a-zA-Z0-9_.,'" \\|*/+-]* # innermost helper arguments ) \) \s* # allow any amount of spaces, for visual clarity @@ -91,24 +98,23 @@ def unregister_helper(name) ) }} ) # closing double braces - /x - - attr_reader :string - attr_reader :passed_data - attr_reader :errors - + }x + + attr_reader :string, :passed_data, :errors + def initialize(string) raise TypeError.new("first argument must be a String, passed #{string.class}") unless string.is_a?(String) @string = string end - + # Executes the logic to recursively replace all handlebars with registered helpers # If there is an error, execution stops and the error is added to @errors # # @return [String] with replaced handlebars def replace(passed_data = {}) raise TypeError.new("passed_data must be a Hash, got #{string.inspect}") unless passed_data.is_a?(Hash) + @passed_data = passed_data @errors = [] @@ -116,15 +122,13 @@ def replace(passed_data = {}) handlebars_array = string.scan(INNERMOST_HELPER_REGEX).map(&:first) handlebars_array.each do |handlebars| - begin - string.sub!(handlebars, eval_handlebars(handlebars)) - rescue => exception - new_exception = exception.class.new( - "#{exception.message} while interpolating '#{handlebars}'" - ) - @errors.push(new_exception) - raise new_exception if @raise_errors - end + string.sub!(handlebars, eval_handlebars(handlebars)) + rescue NoMethodError => e + new_exception = e.class.new( + "#{e.message} while interpolating '#{handlebars}'" + ) + @errors.push(new_exception) + raise new_exception if @raise_errors end string @@ -150,7 +154,7 @@ def helper_exists?(name) end # @return [Boolean] - def is_replaceable + def replaceable? !string.scan(INNERMOST_HELPER_REGEX).empty? end @@ -161,7 +165,7 @@ def is_replaceable def eval_handlebars(handlebars) eval_helpers_recursively(handlebars)[2..-3] end - + # Receives a single 'handlebars' and recusively executes helpers and replaces them # on the main string with the returned value # @@ -170,7 +174,7 @@ def eval_handlebars(handlebars) def eval_helpers_recursively(handlebars) match = handlebars.match(INNERMOST_HELPER_REGEX) return handlebars unless match - + captures = match.named_captures to_replace, helper_name, argument = captures .values_at( @@ -189,9 +193,7 @@ def eval_helpers_recursively(handlebars) # @param [String] argument # @return [String] def eval_helper(name, argument) - if !helper_exists?(name) - raise NoMethodError.new("Unregistered helper '#{name}'") - end + raise NoMethodError.new("Unregistered helper '#{name}'") unless helper_exists?(name) # call the method if argument == '' @@ -210,6 +212,6 @@ def without_quotes(string) .sub(/\A["']/, '') .sub(/["']\z/, '') end - + end end diff --git a/lib/string_replacer/version.rb b/lib/string_replacer/version.rb index 89b7975..7cfe562 100644 --- a/lib/string_replacer/version.rb +++ b/lib/string_replacer/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module StringReplacer - VERSION = "0.4.0" + VERSION = '0.4.1' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..0f608e8 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path('../lib', __dir__) +require 'bundler/setup' +require 'string_replacer' +require 'debug' + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = '.rspec_status' + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end diff --git a/spec/string_replacer/replacer_spec.rb b/spec/string_replacer/replacer_spec.rb new file mode 100644 index 0000000..05b2ece --- /dev/null +++ b/spec/string_replacer/replacer_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +class ::DummyReplacer < StringReplacer::Replacer + register_helper(:capitalize, &:capitalize) + + register_helper(:user_name) do + @passed_data[:user_name] + end + + register_helper(:swapcase, &:swapcase) +end + +class ::DummyReplacerSubclass < DummyReplacer +end + +RSpec.describe StringReplacer::Replacer do + it 'subclasses contain parent registered helpers' do + expect(DummyReplacerSubclass.registered_helpers.first).to be(:capitalize) + expect(DummyReplacerSubclass.registered_helpers.last).to be(:swapcase) + end + + it 'subclasses can call helpers' do + expect( + DummyReplacerSubclass.new('My name is {{capitalize(john)}}').replace + ).to eql('My name is John') + end + + describe '.register_helper' do + it 'can register new helpers' do + DummyReplacerSubclass.register_helper(:downcase, &:downcase) + expect(DummyReplacerSubclass.registered_helpers.last).to be(:downcase) + end + end + + describe '.unregister_helper' do + it 'can unregister existing helpers' do + DummyReplacerSubclass.unregister_helper(:downcase) + expect(DummyReplacerSubclass.registered_helpers.last).to be(:swapcase) + end + end + + describe '#helper_exists?' do + it 'check for existing helpers' do + expect(DummyReplacerSubclass.new('').helper_exists?(:capitalize)).to be(true) + expect(DummyReplacer.new('').helper_exists?(:capitalize)).to be(true) + end + end + + describe '#replace' do + it 'replacer does not modify a string without handlebars' do + string = "I'am a simple string with $ # / & spécial characters?_" + expect(DummyReplacer.new(string).replace).to eql(string) + end + + it 'can call helpers inside handlebars' do + expect( + DummyReplacer.new('My name is {{capitalize(john)}}').replace + ).to eql('My name is John') + end + + it 'can call multiple helpers' do + expect( + DummyReplacer.new('My name is {{capitalize(john)}} {{swapcase(johnson)}}').replace + ).to eql('My name is John JOHNSON') + end + + it 'can call nested helpers' do + expect( + DummyReplacer.new('My name is {{swapcase(capitalize(john))}} {{swapcase(johnson)}}').replace + ).to eql('My name is jOHN JOHNSON') + end + + it 'ignores empty handlebars' do + expect( + DummyReplacer.new('My name is {{}}').replace + ).to eql('My name is {{}}') + end + + it 'ignores unknown helpers and stores errors' do + replacer = DummyReplacer.new('My name is {{some_unknown_helper(capitalize(john))}} {{swapcase(johnson)}}') + expect( + replacer.replace + ).to eql('My name is {{some_unknown_helper(capitalize(john))}} JOHNSON') + expect( + replacer.errors.size + ).not_to eql(0) + end + + it 'can pass data as hash' do + expect( + DummyReplacer.new('My name is {{user_name()}}').replace({user_name: 'Luke'}) + ).to eql('My name is Luke') + end + + it 'ignores space inside handlebar' do + expect( + DummyReplacer.new('My name is {{ user_name() }}').replace({user_name: 'Luke'}) + ).to eql('My name is Luke') + end + end + + describe '#replace!' do + it 'raises error when encounters unknown helper' do + expect { + DummyReplacer.new('My name is {{some_unknown_helper(john)}}').replace! + }.to raise_error(NoMethodError) + end + end + + describe 'replaceable?' do + it 'returns true when it is replaceable' do + expect( + DummyReplacer.new('My name is {{some_unknown_helper(john)}}').replaceable? + ).to be(true) + end + + it 'returns false when it is replaceable' do + expect( + DummyReplacer.new('My name is {{some_unknown_helper(john) ').replaceable? + ).to be(false) + end + end +end \ No newline at end of file diff --git a/spec/string_replacer_spec.rb b/spec/string_replacer_spec.rb deleted file mode 100644 index c25a8ea..0000000 --- a/spec/string_replacer_spec.rb +++ /dev/null @@ -1,154 +0,0 @@ -require 'string_replacer' -require 'debug' - -def remove_classes(*classes) - classes.each do |klass| - Object.send(:remove_const, klass.name.to_sym) if defined?(klass) - end -end - -describe StringReplacer::Replacer do - - # create DummyReplacer and DummyReplacerSubclass - before(:all) do - class ::DummyReplacer < StringReplacer::Replacer - - register_helper(:capitalize) do |argument| - argument.capitalize - end - - register_helper(:user_name) do - @passed_data[:user_name] - end - - register_helper(:swapcase) do |argument| - argument.swapcase - end - - end - - class ::DummyReplacerSubclass < ::DummyReplacer - end - - end - - after(:all) do - remove_classes(::DummyReplacer, ::DummyReplacerSubclass) - end - - it "subclasses contain parent registered helpers" do - expect(::DummyReplacerSubclass.registered_helpers.first).to eql(:capitalize) - expect(::DummyReplacerSubclass.registered_helpers.last).to eql(:swapcase) - end - - it "subclasses can call helpers" do - expect( - ::DummyReplacerSubclass.new('My name is {{capitalize(john)}}').replace - ).to eql('My name is John') - end - - describe '.register_helper' do - it "can register new helpers" do - DummyReplacerSubclass.register_helper(:downcase) do |argument| - argument.downcase - end - expect(::DummyReplacerSubclass.registered_helpers.last).to eql(:downcase) - end - end - - describe '.unregister_helper' do - it "can unregister existing helpers" do - DummyReplacerSubclass.unregister_helper(:downcase) - expect(::DummyReplacerSubclass.registered_helpers.last).to eql(:swapcase) - end - end - - describe '#helper_exists?' do - it "check for existing helpers" do - expect(::DummyReplacerSubclass.new('').helper_exists?(:capitalize)).to eql(true) - expect(::DummyReplacer.new('').helper_exists?(:capitalize)).to eql(true) - end - end - - describe '#replace' do - it "replacer does not modify a string without handlebars" do - string = "I'am a simple string with $ # / & spécial characters?_" - expect(::DummyReplacer.new(string).replace).to eql(string) - end - - it "can call helpers inside handlebars" do - expect( - ::DummyReplacer.new('My name is {{capitalize(john)}}').replace - ).to eql('My name is John') - end - - it "can call multiple helpers" do - expect( - ::DummyReplacer.new('My name is {{capitalize(john)}} {{swapcase(johnson)}}').replace - ).to eql('My name is John JOHNSON') - end - - it "can call nested helpers" do - expect( - ::DummyReplacer.new('My name is {{swapcase(capitalize(john))}} {{swapcase(johnson)}}').replace - ).to eql('My name is jOHN JOHNSON') - end - - it "ignores empty handlebars" do - expect( - ::DummyReplacer.new('My name is {{}}').replace - ).to eql('My name is {{}}') - end - - it "ignores unknown helpers and stores errors" do - replacer = ::DummyReplacer.new('My name is {{some_unknown_helper(capitalize(john))}} {{swapcase(johnson)}}') - expect( - replacer.replace - ).to eql('My name is {{some_unknown_helper(capitalize(john))}} JOHNSON') - expect( - replacer.errors.size - ).not_to eql(0) - end - - it "can pass data as hash" do - expect( - ::DummyReplacer.new('My name is {{user_name()}}').replace({user_name: 'Luke'}) - ).to eql('My name is Luke') - end - - it "ignores space inside handlebar" do - expect( - ::DummyReplacer.new('My name is {{ user_name() }}').replace({user_name: 'Luke'}) - ).to eql('My name is Luke') - end - - end - - describe '#replace!' do - - it 'raises error when encounters unknown helper' do - expect{ - ::DummyReplacer.new('My name is {{some_unknown_helper(john)}}').replace! - }.to raise_error(NoMethodError) - end - - end - - describe 'is_replaceable' do - - it 'returns true when it is replaceable' do - expect( - ::DummyReplacer.new('My name is {{some_unknown_helper(john)}}').is_replaceable - ).to eql(true) - end - - it 'returns false when it is replaceable' do - expect( - ::DummyReplacer.new('My name is {{some_unknown_helper(john) ').is_replaceable - ).to eql(false) - end - - end - - -end \ No newline at end of file diff --git a/string_replacer.gemspec b/string_replacer.gemspec index 6a5c31c..5e49201 100644 --- a/string_replacer.gemspec +++ b/string_replacer.gemspec @@ -1,42 +1,33 @@ +# frozen_string_literal: true -lib = File.expand_path("../lib", __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "string_replacer/version" +require_relative 'lib/string_replacer/version' Gem::Specification.new do |spec| - spec.name = "string_replacer" + spec.name = 'string_replacer' spec.version = StringReplacer::VERSION - spec.authors = ["Pato"] - spec.email = ["pato_devilla@hotmail.com"] + spec.authors = ['Pato'] + spec.email = ['pato_devilla@hotmail.com'] - spec.summary = %q{Safely replace strings} - spec.description = %q{Safely replace strings} + spec.summary = 'Safely replace strings' + spec.description = 'Safely replace strings' # spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.license = 'MIT' + spec.required_ruby_version = '>= 3.1.0' - # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' - # to allow pushing to a single host or delete this section to allow pushing to any host. - # if spec.respond_to?(:metadata) - # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" - - # spec.metadata["homepage_uri"] = spec.homepage - # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." - # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." - # else - # raise "RubyGems 2.0 or newer is required to protect against " \ - # "public gem pushes." - # end + # spec.metadata['homepage_uri'] = spec.homepage + # spec.metadata['source_code_uri'] = spec.homepage + # spec.metadata['changelog_uri'] = 'https://github.com/apartmentlist/sidekiq-bouncer/blob/master/CHANGELOG.md' + spec.metadata['rubygems_mfa_required'] = 'true' # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.files = Dir.chdir(__dir__) do + `git ls-files -z`.split("\x0").reject do |f| + (File.expand_path(f) == __FILE__) || + f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile]) + end end - spec.bindir = "exe" + spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] - - spec.add_development_dependency "bundler", "2.4.22" - spec.add_development_dependency "rake", "~> 13.1" - spec.add_development_dependency "debug", ">= 1.0.0" - spec.add_development_dependency "rspec", "~> 3.9" + spec.require_paths = ['lib'] end