From 4e48a91d4430ffdaf5482e6fe66f9e7126fb74af Mon Sep 17 00:00:00 2001 From: Guilherme Souza <101073+guilhermef@users.noreply.github.com> Date: Thu, 27 Jan 2022 01:40:06 +0100 Subject: [PATCH 1/6] Update gemspec --- Gemfile | 1 - ruby-thumbor.gemspec | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index 578ee99..4ac9a4c 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,5 @@ source 'https://rubygems.org' gemspec -gem 'rspec' gem 'simplecov', require: false gem 'simplecov-lcov', require: false diff --git a/ruby-thumbor.gemspec b/ruby-thumbor.gemspec index c4b61d2..d939df8 100644 --- a/ruby-thumbor.gemspec +++ b/ruby-thumbor.gemspec @@ -1,7 +1,6 @@ -# -*- encoding: utf-8 -*- -lib = File.expand_path('../lib/', __FILE__) -$:.unshift lib unless $:.include?(lib) -require 'thumbor/version' +# frozen_string_literal: true + +require_relative 'lib/thumbor/version.rb' Gem::Specification.new do |s| s.name = 'ruby-thumbor' @@ -9,11 +8,33 @@ Gem::Specification.new do |s| s.authors = ['Bernardo Heynemann', 'Guilherme Souza'] s.description = 'ruby-thumbor is the client to the thumbor imaging service (http://github.com/thumbor/thumbor).' - s.email = ['heynemann@gmail.com', 'guivideojob@gmail.com'] - s.files = Dir.glob('lib/**/*.rb') << 'README.rdoc' + s.email = ['heynemann@gmail.com', 'guilherme@souza.tech'] + s.license = "MIT" + + s.files = Dir.glob('lib/**/*.rb') s.test_files = Dir.glob('spec/**/*.rb') + s.homepage = 'http://github.com/thumbor/ruby-thumbor' - s.rdoc_options = ['--main', 'README.rdoc'] + + s.extra_rdoc_files = Dir["README.md"] + s.rdoc_options += [ + "--title", "Ruby-Thumbor", + "--main", "README.md", + "--line-numbers", + "--inline-source", + "--quiet" + ] + s.summary = 'ruby-thumbor is the client to the thumbor imaging service (http://github.com/thumbor/thumbor).' - s.add_development_dependency('rake') + + s.metadata = { + "bug_tracker_uri" => "https://github.com/thumbor/ruby-thumbor/issues", + "documentation_uri" => "https://www.rubydoc.info/gems/ruby-thumbor", + "homepage_uri" => s.homepage, + "source_code_uri" => "https://github.com/thumbor/ruby-thumbor" + } + + s.add_development_dependency 'rake', '~> 13.0' + s.add_development_dependency 'rspec', '~> 3.10' + end From 5469415689b724e42352d116192e7d5d8db7c612 Mon Sep 17 00:00:00 2001 From: Guilherme Souza <101073+guilhermef@users.noreply.github.com> Date: Thu, 27 Jan 2022 02:23:24 +0100 Subject: [PATCH 2/6] Running rubocop --- .rubocop.yml | 14 ++ Gemfile | 2 + Rakefile | 8 +- lib/ruby-thumbor.rb | 2 + lib/thumbor/cascade.rb | 48 ++--- lib/thumbor/crypto_url.rb | 340 +++++++++++++++++------------------ lib/thumbor/version.rb | 2 + ruby-thumbor.gemspec | 25 ++- spec/spec_helper.rb | 13 +- spec/thumbor/cascade_spec.rb | 138 +++++++------- tasks/build.rake | 16 +- tasks/rspec.rake | 2 + 12 files changed, 317 insertions(+), 293 deletions(-) create mode 100644 .rubocop.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..fc18d2d --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,14 @@ +Naming/FileName: + Enabled: false + +Style/Documentation: + Enabled: false + +Metrics/ClassLength: + Max: 130 + +Layout/LineLength: + Max: 161 + +Metrics/BlockLength: + Max: 255 diff --git a/Gemfile b/Gemfile index 4ac9a4c..0caa3fc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec diff --git a/Rakefile b/Rakefile index c7f1131..e386165 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ -Dir.glob('tasks/*.rake').each { |r| import r} - -task :default => :spec +# frozen_string_literal: true + +Dir.glob('tasks/*.rake').each { |r| import r } + +task default: :spec diff --git a/lib/ruby-thumbor.rb b/lib/ruby-thumbor.rb index 85bf24e..53a5b2c 100644 --- a/lib/ruby-thumbor.rb +++ b/lib/ruby-thumbor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'thumbor/crypto_url' require 'thumbor/cascade' diff --git a/lib/thumbor/cascade.rb b/lib/thumbor/cascade.rb index 5f979f0..ad72174 100644 --- a/lib/thumbor/cascade.rb +++ b/lib/thumbor/cascade.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'forwardable' require 'openssl' require 'base64' @@ -8,14 +10,17 @@ module Thumbor class Cascade attr_accessor :image, :crypto, :options, :filters - @available_options = [ - :meta, :crop, :center, - :original_width, :original_height, - :width, :height, :flip, - :flop, :halign, :valign, - :smart, :fit_in, :adaptive_fit_in, - :full_fit_in, :adaptive_full_fit_in, - :trim, :debug] + FILTER_REGEX = Regexp.new('^(.+)_filter$') + + @available_options = %i[ + meta crop center + original_width original_height + width height flip + flop halign valign + smart fit_in adaptive_fit_in + full_fit_in adaptive_full_fit_in + trim debug + ] extend Forwardable @@ -29,7 +34,7 @@ class Cascade end end - def initialize(key=false, image=nil) + def initialize(key = nil, image = nil) @key = key @image = image @options = {} @@ -38,36 +43,39 @@ def initialize(key=false, image=nil) end def generate - @crypto.generate prepare_options(@options).merge({image: @image, filters: @filters}) + @crypto.generate prepare_options(@options).merge({ image: @image, filters: @filters }) end - def method_missing(m, *args) - if /^(.+)_filter$/.match(m.to_s) - @filters << "#{$1}(#{escape_args(args).join(',')})" + def method_missing(method, *args) + if method =~ FILTER_REGEX + @filters << "#{FILTER_REGEX.match(method)[1]}(#{escape_args(args).join(',')})" self else super end end + def respond_to_missing?(method, include_private = false) + method =~ FILTER_REGEX || super + end + private def escape_args(args) args.map do |arg| - arg = CGI::escape(arg) if arg.is_a? String and arg.match(/^https?:\/\//) + arg = CGI.escape(arg) if arg.is_a?(String) && arg.match(%r{^https?://}) arg end end def prepare_options(options) - options.reduce({}) do |final_options, item| + options.each_with_object({}) do |item, final_options| value = if item[1].length == 1 - item[1].first - else - item[1] - end + item[1].first + else + item[1] + end final_options[item[0]] = value - final_options end end end diff --git a/lib/thumbor/crypto_url.rb b/lib/thumbor/crypto_url.rb index 280b160..a5a11b4 100644 --- a/lib/thumbor/crypto_url.rb +++ b/lib/thumbor/crypto_url.rb @@ -1,216 +1,198 @@ +# frozen_string_literal: true + require 'openssl' require 'base64' require 'digest/md5' require 'cgi' module Thumbor - class CryptoURL - attr_writer :computed_key + class CryptoURL + attr_writer :computed_key - def initialize(key=false) - @key = key - end + def initialize(key = nil) + @key = key + end - def computed_key - (@key * 16)[0..15] - end + def computed_key + (@key * 16)[0..15] + end + + def calculate_width_and_height(url_parts, options) + width = options[:width] + height = options[:height] + + width *= -1 if width && options[:flip] + height *= -1 if height && options[:flop] + + if width || height + width ||= 0 + height ||= 0 + end + + has_width = width + has_height = height + if options[:flip] && !has_width && !has_height + width = '-0' + height = '0' if !has_height && !(options[:flop]) + end + if options[:flop] && !has_width && !has_height + height = '-0' + width = '0' if !has_width && !(options[:flip]) + end + + url_parts.push("#{width}x#{height}") if width || height + end - def calculate_width_and_height(url_parts, options) - width = options[:width] - height = options[:height] - - if width and options[:flip] - width = width * -1 - end - if height and options[:flop] - height = height * -1 - end - - if width or height - width = 0 if not width - height = 0 if not height - end - - has_width = width - has_height = height - if options[:flip] and not has_width and not has_height - width = "-0" - height = '0' if not has_height and not options[:flop] - end - if options[:flop] and not has_width and not has_height - height = "-0" - width = '0' if not has_width and not options[:flip] - end - - url_parts.push("#{width}x#{height}") if width || height + def calculate_centered_crop(options) + width = options[:width] + height = options[:height] + original_width = options[:original_width] + original_height = options[:original_height] + center = options[:center] + + return unless original_width && + original_height && + center && + (width || height) + + raise 'center must be an array of x,y' unless center.is_a?(Array) && center.length == 2 + + center_x, center_y = center + width ||= original_width + height ||= original_height + width = width.abs + height = height.abs + new_aspect_ratio = width / height.to_f + original_aspect_ratio = original_width / original_height.to_f + + crop = nil + # We're going wider, vertical crop + if new_aspect_ratio > original_aspect_ratio + # How tall should we be? because new_aspect_ratio is > original_aspect_ratio we can be sure + # that cropped_height is always less than original_height (original). This is assumed below. + cropped_height = (original_width / new_aspect_ratio).round + # Calculate coordinates around center + top_crop_point = (center_y - (cropped_height * 0.5)).round + bottom_crop_point = (center_y + (cropped_height * 0.5)).round + + # If we've gone above the top of the image, take all from the bottom + if top_crop_point.negative? + top_crop_point = 0 + bottom_crop_point = cropped_height + # If we've gone below the top of the image, take all from the top + elsif bottom_crop_point > original_height + top_crop_point = original_height - cropped_height + bottom_crop_point = original_height + # Because cropped_height < original_height, top_crop_point and + # bottom_crop_point will never both be out of bounds end - def calculate_centered_crop(options) - width = options[:width] - height = options[:height] - original_width = options[:original_width] - original_height = options[:original_height] - center = options[:center] - - return unless original_width && - original_height && - center && - (width || height) - - unless center.kind_of?(Array) && center.length == 2 - raise 'center must be an array of x,y' - end - - center_x, center_y = center - width ||= original_width - height ||= original_height - width = width.abs - height = height.abs - new_aspect_ratio = width / height.to_f - original_aspect_ratio = original_width/original_height.to_f - - crop = nil - # We're going wider, vertical crop - if new_aspect_ratio > original_aspect_ratio - # How tall should we be? because new_aspect_ratio is > original_aspect_ratio we can be sure - # that cropped_height is always less than original_height (original). This is assumed below. - cropped_height = (original_width / new_aspect_ratio).round - # Calculate coordinates around center - top_crop_point = (center_y - (cropped_height * 0.5)).round - bottom_crop_point = (center_y + (cropped_height * 0.5)).round - - # If we've gone above the top of the image, take all from the bottom - if top_crop_point < 0 - top_crop_point = 0 - bottom_crop_point = cropped_height - # If we've gone below the top of the image, take all from the top - elsif bottom_crop_point > original_height - top_crop_point = original_height - cropped_height - bottom_crop_point = original_height - # Because cropped_height < original_height, top_crop_point and - # bottom_crop_point will never both be out of bounds - end - - # Put it together - crop = [0, top_crop_point, original_width, bottom_crop_point] - # We're going taller, horizontal crop - elsif new_aspect_ratio < original_aspect_ratio - # How wide should we be? because new_aspect_ratio is < original_aspect_ratio we can be sure - # that cropped_width is always less than original_width (original). This is assumed below. - cropped_width = (original_height * new_aspect_ratio).round - # Calculate coordinates around center - left_crop_point = (center_x - (cropped_width * 0.5)).round - right_crop_point = (center_x + (cropped_width * 0.5)).round - - # If we've gone beyond the left of the image, take all from the right - if left_crop_point < 0 - left_crop_point = 0 - right_crop_point = cropped_width - # If we've gone beyond the right of the image, take all from the left - elsif right_crop_point > original_width - left_crop_point = original_width - cropped_width - right_crop_point = original_width - # Because cropped_width < original_width, left_crop_point and - # right_crop_point will never both be out of bounds - end - - # Put it together - crop = [left_crop_point, 0, right_crop_point, original_height] - end - - options[:crop] = crop + # Put it together + crop = [0, top_crop_point, original_width, bottom_crop_point] + # We're going taller, horizontal crop + elsif new_aspect_ratio < original_aspect_ratio + # How wide should we be? because new_aspect_ratio is < original_aspect_ratio we can be sure + # that cropped_width is always less than original_width (original). This is assumed below. + cropped_width = (original_height * new_aspect_ratio).round + # Calculate coordinates around center + left_crop_point = (center_x - (cropped_width * 0.5)).round + right_crop_point = (center_x + (cropped_width * 0.5)).round + + # If we've gone beyond the left of the image, take all from the right + if left_crop_point.negative? + left_crop_point = 0 + right_crop_point = cropped_width + # If we've gone beyond the right of the image, take all from the left + elsif right_crop_point > original_width + left_crop_point = original_width - cropped_width + right_crop_point = original_width + # Because cropped_width < original_width, left_crop_point and + # right_crop_point will never both be out of bounds end - def url_for(options) - if not options[:image] - raise 'image is a required argument.' - end + # Put it together + crop = [left_crop_point, 0, right_crop_point, original_height] + end - url_parts = Array.new + options[:crop] = crop + end - if options[:debug] - url_parts.push('debug') - end + def url_for(options) + raise 'image is a required argument.' unless options[:image] - if options[:trim] - trim_options = ['trim'] - trim_options << options[:trim] unless options[:trim] == true or options[:trim][0] == true - url_parts.push(trim_options.join(':')) - end + url_parts = [] - if options[:meta] - url_parts.push('meta') - end + url_parts.push('debug') if options[:debug] - calculate_centered_crop(options) + if options[:trim] + trim_options = ['trim'] + trim_options << options[:trim] unless (options[:trim] == true) || (options[:trim][0] == true) + url_parts.push(trim_options.join(':')) + end - crop = options[:crop] - if crop - crop_left = crop[0] - crop_top = crop[1] - crop_right = crop[2] - crop_bottom = crop[3] + url_parts.push('meta') if options[:meta] - if crop_left > 0 or crop_top > 0 or crop_bottom > 0 or crop_right > 0 - url_parts.push(crop_left.to_s << 'x' << crop_top.to_s << ':' << crop_right.to_s << 'x' << crop_bottom.to_s) - end - end + calculate_centered_crop(options) - [:fit_in, :adaptive_fit_in, :full_fit_in, :adaptive_full_fit_in].each do |fit| - if options[fit] - url_parts.push(fit.to_s.gsub('_','-')) - end - end + crop = options[:crop] + if crop + crop_left = crop[0] + crop_top = crop[1] + crop_right = crop[2] + crop_bottom = crop[3] - if options.include?(:fit_in) or options.include?(:full_fit_in) and not (options.include?(:width) or options.include?(:height)) - raise ArgumentError, 'When using fit-in or full-fit-in, you must specify width and/or height.' - end + if crop_left.positive? || crop_top.positive? || crop_bottom.positive? || crop_right.positive? + url_parts.push(crop_left.to_s << 'x' << crop_top.to_s << ':' << crop_right.to_s << 'x' << crop_bottom.to_s) + end + end - calculate_width_and_height(url_parts, options) + %i[fit_in adaptive_fit_in full_fit_in adaptive_full_fit_in].each do |fit| + url_parts.push(fit.to_s.gsub('_', '-')) if options[fit] + end - if options[:halign] and options[:halign] != :center - url_parts.push(options[:halign]) - end + if (options.include?(:fit_in) || options.include?(:full_fit_in)) && !(options.include?(:width) || options.include?(:height)) + raise ArgumentError, 'When using fit-in or full-fit-in, you must specify width and/or height.' + end - if options[:valign] and options[:valign] != :middle - url_parts.push(options[:valign]) - end + calculate_width_and_height(url_parts, options) - if options[:smart] - url_parts.push('smart') - end + url_parts.push(options[:halign]) if options[:halign] && (options[:halign] != :center) - if options[:filters] && !options[:filters].empty? - filter_parts = [] - options[:filters].each do |filter| - filter_parts.push(filter) - end + url_parts.push(options[:valign]) if options[:valign] && (options[:valign] != :middle) - url_parts.push("filters:#{ filter_parts.join(':') }") - end + url_parts.push('smart') if options[:smart] - return url_parts.join('/') + if options[:filters] && !options[:filters].empty? + filter_parts = [] + options[:filters].each do |filter| + filter_parts.push(filter) end - def url_safe_base64(str) - Base64.encode64(str).gsub('+', '-').gsub('/', '_').gsub!(/[\n]/, '') - end + url_parts.push("filters:#{filter_parts.join(':')}") + end - def generate(options) - thumbor_path = "" + url_parts.join('/') + end - image_options = url_for(options) - thumbor_path << image_options + '/' unless image_options.empty? + def url_safe_base64(str) + Base64.encode64(str).gsub('+', '-').gsub('/', '_').gsub!(/\n/, '') + end - thumbor_path << options[:image] + def generate(options) + thumbor_path = [] - if @key - signature = url_safe_base64(OpenSSL::HMAC.digest('sha1', @key, thumbor_path)) - thumbor_path.insert(0, "/#{signature}/") - else - thumbor_path.insert(0, "/unsafe/") - end - thumbor_path - end + image_options = url_for(options) + thumbor_path << "#{image_options}/" unless image_options.empty? + + thumbor_path << options[:image] + + if @key.nil? + thumbor_path.insert(0, '/unsafe/') + else + signature = url_safe_base64(OpenSSL::HMAC.digest('sha1', @key, thumbor_path.join)) + thumbor_path.insert(0, "/#{signature}/") + end + thumbor_path.join end + end end diff --git a/lib/thumbor/version.rb b/lib/thumbor/version.rb index 3a66f24..c1b1ed7 100644 --- a/lib/thumbor/version.rb +++ b/lib/thumbor/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Thumbor VERSION = '3.0.0' end diff --git a/ruby-thumbor.gemspec b/ruby-thumbor.gemspec index d939df8..138747b 100644 --- a/ruby-thumbor.gemspec +++ b/ruby-thumbor.gemspec @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'lib/thumbor/version.rb' +require_relative 'lib/thumbor/version' Gem::Specification.new do |s| s.name = 'ruby-thumbor' @@ -9,32 +9,31 @@ Gem::Specification.new do |s| s.authors = ['Bernardo Heynemann', 'Guilherme Souza'] s.description = 'ruby-thumbor is the client to the thumbor imaging service (http://github.com/thumbor/thumbor).' s.email = ['heynemann@gmail.com', 'guilherme@souza.tech'] - s.license = "MIT" + s.license = 'MIT' s.files = Dir.glob('lib/**/*.rb') s.test_files = Dir.glob('spec/**/*.rb') s.homepage = 'http://github.com/thumbor/ruby-thumbor' - s.extra_rdoc_files = Dir["README.md"] + s.extra_rdoc_files = Dir['README.md'] s.rdoc_options += [ - "--title", "Ruby-Thumbor", - "--main", "README.md", - "--line-numbers", - "--inline-source", - "--quiet" + '--title', 'Ruby-Thumbor', + '--main', 'README.md', + '--line-numbers', + '--inline-source', + '--quiet' ] s.summary = 'ruby-thumbor is the client to the thumbor imaging service (http://github.com/thumbor/thumbor).' s.metadata = { - "bug_tracker_uri" => "https://github.com/thumbor/ruby-thumbor/issues", - "documentation_uri" => "https://www.rubydoc.info/gems/ruby-thumbor", - "homepage_uri" => s.homepage, - "source_code_uri" => "https://github.com/thumbor/ruby-thumbor" + 'bug_tracker_uri' => 'https://github.com/thumbor/ruby-thumbor/issues', + 'documentation_uri' => 'https://www.rubydoc.info/gems/ruby-thumbor', + 'homepage_uri' => s.homepage, + 'source_code_uri' => 'https://github.com/thumbor/ruby-thumbor' } s.add_development_dependency 'rake', '~> 13.0' s.add_development_dependency 'rspec', '~> 3.10' - end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2227c45..9eae752 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'simplecov' require 'simplecov-lcov' @@ -7,10 +8,12 @@ config.single_report_path = 'coverage/lcov.info' end -SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter[ - SimpleCov::Formatter::HTMLFormatter, - SimpleCov::Formatter::LcovFormatter, -] +SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new( + [ + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::LcovFormatter + ] +) SimpleCov.start do add_filter '/spec/' @@ -21,6 +24,6 @@ end RSpec.configure do |c| - c.filter_run :focus => true + c.filter_run focus: true c.run_all_when_everything_filtered = true end diff --git a/spec/thumbor/cascade_spec.rb b/spec/thumbor/cascade_spec.rb index fb91d7f..43fa4cc 100644 --- a/spec/thumbor/cascade_spec.rb +++ b/spec/thumbor/cascade_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'json' require 'ruby-thumbor' @@ -9,310 +11,314 @@ subject { Thumbor::Cascade.new key, image_url } describe '#new' do - - it "should create a new instance passing key and keep it" do + it 'should create a new instance passing key and keep it' do expect(subject.computed_key).to eq 'my-security-keym' end end it 'should raise an error' do - expect{ subject.god_of_war_crop }.to raise_error(NoMethodError) + expect { subject.god_of_war_crop }.to raise_error(NoMethodError) end - describe '#generate' do + it 'should respond to filter methods' do + expect(subject.respond_to?('quality_filter')).to be_truthy + end - it "should create an unsafe url" do - url = Thumbor::Cascade.new(false, image_url).width(300).height(200).generate + describe '#generate' do + it 'should create an unsafe url' do + url = Thumbor::Cascade.new(nil, image_url).width(300).height(200).generate expect(url).to eq '/unsafe/300x200/my.domain.com/some/image/url.jpg' end - it "should create an url with debug" do + it 'should create an url with debug' do url = subject.debug(true).height(200).generate expect(url).to eq '/5_eX4HHQYk81HQVkc1gBIAvPbLo=/debug/0x200/my.domain.com/some/image/url.jpg' end - it "should create a new instance passing key and keep it" do + it 'should create a new instance passing key and keep it' do url = subject.width(300).height(200).generate expect(url).to eq '/TQfyd3H36Z3srcNcLOYiM05YNO8=/300x200/my.domain.com/some/image/url.jpg' end - it "should be able to change the Thumbor key" do + it 'should be able to change the Thumbor key' do url1 = subject.width(300).height(200).generate url2 = Thumbor::Cascade.new('another-thumbor-key', image_url).width(300).height(200).generate expect(url1).not_to eq url2 end - it "should create a new instance passing key and keep it" do + it 'should create a new instance passing key and keep it' do url = subject.width(300).height(200).meta(true).generate expect(url).to eq '/YBQEWd3g_WRMnVEG73zfzcr8Zj0=/meta/300x200/my.domain.com/some/image/url.jpg' end - it "should create a new instance passing key and keep it" do + it 'should create a new instance passing key and keep it' do url = subject.width(300).height(200).meta(true).smart(true).generate expect(url).to eq '/jP89J0qOWHgPlm_lOA28GtOh5GU=/meta/300x200/smart/my.domain.com/some/image/url.jpg' end - it "should create a new instance passing key and keep it" do + it 'should create a new instance passing key and keep it' do url = subject.width(300).height(200).meta(true).smart(true).fit_in(true).generate expect(url).to eq '/zrrOh_TtTs4kiLLEQq1w4bcTYdc=/meta/fit-in/300x200/smart/my.domain.com/some/image/url.jpg' end - it "should create a new instance passing key and keep it" do + it 'should create a new instance passing key and keep it' do url = subject.width(300).height(200).meta(true).smart(true).fit_in(true).flip(true).generate expect(url).to eq '/4t1XK1KH43cOb1QJ9tU00-W2_k8=/meta/fit-in/-300x200/smart/my.domain.com/some/image/url.jpg' end - it "should create a new instance passing key and keep it" do + it 'should create a new instance passing key and keep it' do url = subject.width(300).height(200).meta(true).smart(true).fit_in(true).flip(true).flop(true).generate expect(url).to eq '/HJnvjZU69PkPOhyZGu-Z3Uc_W_A=/meta/fit-in/-300x-200/smart/my.domain.com/some/image/url.jpg' end - it "should create a new instance passing key and keep it" do + it 'should create a new instance passing key and keep it' do url = subject.quality_filter(20).brightness_filter(10).generate expect(url).to eq '/q0DiFg-5-eFZIqyN3lRoCvg2K0s=/filters:quality(20):brightness(10)/my.domain.com/some/image/url.jpg' end - it "should return just the image hash if no arguments passed" do + it 'should return just the image hash if no arguments passed' do url = subject.generate expect(url).to eq '/964rCTkAEDtvjy_a572k7kRa0SU=/my.domain.com/some/image/url.jpg' end - it "should raise if no image passed" do + it 'should raise if no image passed' do expect { Thumbor::Cascade.new.generate }.to raise_error(RuntimeError) end - it "should return proper url for width-only" do + it 'should return proper url for width-only' do url = subject.width(300).generate expect(url).to eq '/eFwrBWryxtRw9hDSbQPhi5iLpo8=/300x0/my.domain.com/some/image/url.jpg' end - it "should return proper url for height-only" do + it 'should return proper url for height-only' do url = subject.height(300).generate expect(url).to eq '/-VGIgp7g8cMKcfF2gFK9ZpmB_5w=/0x300/my.domain.com/some/image/url.jpg' end - it "should return proper url for width and height" do + it 'should return proper url for width and height' do url = subject.width(200).height(300).generate expect(url).to eq '/TrM0qqfcivb6VxS3Hxlxn43W21k=/200x300/my.domain.com/some/image/url.jpg' end - it "should return proper smart url" do + it 'should return proper smart url' do url = subject.width(200).height(300).smart(true).generate expect(url).to eq '/hdzhxXzK45DzNTru5urV6x6xxSs=/200x300/smart/my.domain.com/some/image/url.jpg' end - it "should return proper fit-in url" do + it 'should return proper fit-in url' do url = subject.width(200).height(300).fit_in(true).generate expect(url).to eq '/LOv6ArMOJA2VRpfMQjjs4xSyZpM=/fit-in/200x300/my.domain.com/some/image/url.jpg' end - it "should return proper adaptive-fit-in url" do + it 'should return proper adaptive-fit-in url' do url = subject.width(200).height(300).adaptive_fit_in(true).generate expect(url).to eq '/V2xmSmQZm4i5-0Flx8iuRtawOkg=/adaptive-fit-in/200x300/my.domain.com/some/image/url.jpg' end - it "should return proper full-fit-in url" do + it 'should return proper full-fit-in url' do url = subject.width(200).height(300).full_fit_in(true).generate expect(url).to eq '/geXhR7ZFxztQTsKzmkDxYCX-HHg=/full-fit-in/200x300/my.domain.com/some/image/url.jpg' end - it "should return proper adaptive-full-fit-in url" do + it 'should return proper adaptive-full-fit-in url' do url = subject.width(200).height(300).adaptive_full_fit_in(true).generate expect(url).to eq '/jlUfjdC-6rG6jmuHgFp6eKgPy2g=/adaptive-full-fit-in/200x300/my.domain.com/some/image/url.jpg' end - [:fit_in, :full_fit_in].each do |fit| + %i[fit_in full_fit_in].each do |fit| it "should raise error when using #{fit} without width or height" do subject.send(fit, true) - expect{subject.generate}.to raise_error(ArgumentError) + expect { subject.generate }.to raise_error(ArgumentError) end end - it "should return proper flip url if no width and height" do + it 'should return proper flip url if no width and height' do url = subject.flip(true).generate expect(url).to eq '/rlI4clPR-p-PR2QAxj5ZWiTfvH4=/-0x0/my.domain.com/some/image/url.jpg' end - it "should return proper flop url if no width and height" do + it 'should return proper flop url if no width and height' do url = subject.flop(true).generate expect(url).to eq '/-4dmGj-TwIEqTAL9_9yEqUM8cks=/0x-0/my.domain.com/some/image/url.jpg' end - it "should return proper flip-flop url if no width and height" do + it 'should return proper flip-flop url if no width and height' do url = subject.flip(true).flop(true).generate expect(url).to eq '/FnMxpQMmxiMpdG219Dsj8pD_4Xc=/-0x-0/my.domain.com/some/image/url.jpg' end - it "should return proper flip url if width" do + it 'should return proper flip url if width' do url = subject.width(300).flip(true).generate expect(url).to eq '/ccr2PoSYcTEGL4_Wzt4u3wWVRKU=/-300x0/my.domain.com/some/image/url.jpg' end - it "should return proper flop url if height" do + it 'should return proper flop url if height' do url = subject.height(300).flop(true).generate expect(url).to eq '/R5K91tkyNgXO65F6E0txgA6C9lY=/0x-300/my.domain.com/some/image/url.jpg' end - it "should return horizontal align" do + it 'should return horizontal align' do url = subject.halign(:left).generate expect(url).to eq '/GTJE3wUt3sURik0O9Nae8sfI928=/left/my.domain.com/some/image/url.jpg' end - it "should not return horizontal align if it is center" do + it 'should not return horizontal align if it is center' do url = subject.halign(:center).generate expect(url).to eq '/964rCTkAEDtvjy_a572k7kRa0SU=/my.domain.com/some/image/url.jpg' end - it "should return vertical align" do + it 'should return vertical align' do url = subject.valign(:top).generate expect(url).to eq '/1QQo5ihObuhgwl95--Z3g78vjiE=/top/my.domain.com/some/image/url.jpg' end - it "should not return vertical align if it is middle" do + it 'should not return vertical align if it is middle' do url = subject.valign(:middle).generate expect(url).to eq '/964rCTkAEDtvjy_a572k7kRa0SU=/my.domain.com/some/image/url.jpg' end - it "should return halign and valign properly" do + it 'should return halign and valign properly' do url = subject.halign(:left).valign(:top).generate expect(url).to eq '/yA2rmtWv_uzHd9klz5OuMIZ5auI=/left/top/my.domain.com/some/image/url.jpg' end - it "should return meta properly" do + it 'should return meta properly' do url = subject.meta(true).generate expect(url).to eq '/WvIJFDJDePgIl5hZcLgtrzIPxfY=/meta/my.domain.com/some/image/url.jpg' end - it "should return proper crop url when param is array" do + it 'should return proper crop url when param is array' do url = subject.crop([10, 20, 30, 40]).generate expect(url).to eq '/QcnhqNfHwiP6BHLbD6UvneX7K28=/10x20:30x40/my.domain.com/some/image/url.jpg' end - it "should return proper crop url" do + it 'should return proper crop url' do url = subject.crop(10, 20, 30, 40).generate expect(url).to eq '/QcnhqNfHwiP6BHLbD6UvneX7K28=/10x20:30x40/my.domain.com/some/image/url.jpg' end - it "should ignore crop if all zeros" do + it 'should ignore crop if all zeros' do url = subject.crop(0, 0, 0, 0).generate expect(url).to eq '/964rCTkAEDtvjy_a572k7kRa0SU=/my.domain.com/some/image/url.jpg' end - it "should have smart after halign and valign" do + it 'should have smart after halign and valign' do url = subject.halign(:left).valign(:top).smart(true).generate expect(url).to eq '/KS6mVuzlGE3hJ75n3JUonfGgSFM=/left/top/smart/my.domain.com/some/image/url.jpg' end - it "should have quality filter" do + it 'should have quality filter' do url = subject.quality_filter(20).generate expect(url).to eq '/NyA-is4NojxiRqo0NcmJDhB6GTs=/filters:quality(20)/my.domain.com/some/image/url.jpg' end - it "should have brightness filter" do + it 'should have brightness filter' do url = subject.brightness_filter(30).generate expect(url).to eq '/oXDmnGD7vQV-rXcj8kCl1tcS3jc=/filters:brightness(30)/my.domain.com/some/image/url.jpg' end - it "should have 2 filters" do + it 'should have 2 filters' do url = subject.brightness_filter(30).quality_filter(20).generate expect(url).to eq '/SW9o4xQG1QAzE69WzEzarL_G3MI=/filters:brightness(30):quality(20)/my.domain.com/some/image/url.jpg' end - it "should escape url args" do + it 'should escape url args' do url = subject.watermark_filter('http://my-server.com/image.png', 30).quality_filter(20).generate expect(url).to eq '/4b9kwg0-zsojf7Ed01TPKPYOel4=/filters:watermark(http%3A%2F%2Fmy-server.com%2Fimage.png,30):quality(20)/my.domain.com/some/image/url.jpg' end - it "should have trim without params" do + it 'should have trim without params' do url = subject.trim.generate expect(url).to eq '/w23BC0dUiYBFrUnuoYJe8XROuyw=/trim/my.domain.com/some/image/url.jpg' end - it "should have trim with direction param" do + it 'should have trim with direction param' do url = subject.trim('bottom-right').generate expect(url).to eq '/kXPwSmqEvPFQezgzBCv9BtPWmBY=/trim:bottom-right/my.domain.com/some/image/url.jpg' end - it "should have trim with direction and tolerance param" do + it 'should have trim with direction and tolerance param' do url = subject.trim('bottom-right', 15).generate expect(url).to eq '/TUCEIhtWfI1Uv9zjavCSl_i0A_8=/trim:bottom-right:15/my.domain.com/some/image/url.jpg' end - it "should have the right crop when cropping horizontally and given a left center" do + it 'should have the right crop when cropping horizontally and given a left center' do url = subject.original_width(100).original_height(100).width(40).height(50).center(0, 50).generate expect(url).to eq '/SZIT3w4Qgebv5DuVJ8G1IH1mkCU=/0x0:80x100/40x50/my.domain.com/some/image/url.jpg' end - it "should have the right crop when cropping horizontally and given a right center" do + it 'should have the right crop when cropping horizontally and given a right center' do url = subject.original_width(100).original_height(100).width(40).height(50).center(100, 50).generate expect(url).to eq '/NEtCYehaISE7qR3zFj15CxnZoCs=/20x0:100x100/40x50/my.domain.com/some/image/url.jpg' end - it "should have the right crop when cropping horizontally and given the actual center" do + it 'should have the right crop when cropping horizontally and given the actual center' do url = subject.original_width(100).original_height(100).width(40).height(50).center(50, 50).generate expect(url).to eq '/JLH65vJTu6d-cXBmqe5hYoSD4ho=/10x0:90x100/40x50/my.domain.com/some/image/url.jpg' end - it "should have the right crop when cropping vertically and given a top center" do + it 'should have the right crop when cropping vertically and given a top center' do url = subject.original_width(100).original_height(100).width(50).height(40).center(50, 0).generate expect(url).to eq '/FIMZcLatW6bjgSRH9xTkEwUZAZ8=/0x0:100x80/50x40/my.domain.com/some/image/url.jpg' end - it "should have the right crop when cropping vertically and given a bottom center" do + it 'should have the right crop when cropping vertically and given a bottom center' do url = subject.original_width(100).original_height(100).width(50).height(40).center(50, 100).generate expect(url).to eq '/9Ud0sVo6i9DLOjlKbQP_4JXgFmA=/0x20:100x100/50x40/my.domain.com/some/image/url.jpg' end - it "should have the right crop when cropping vertically and given the actual center" do + it 'should have the right crop when cropping vertically and given the actual center' do url = subject.original_width(100).original_height(100).width(50).height(40).center(50, 50).generate expect(url).to eq '/WejLJn8djJLn7DkMUq3S0zZCvZE=/0x10:100x90/50x40/my.domain.com/some/image/url.jpg' end - it "should have the no crop when not necessary" do + it 'should have the no crop when not necessary' do url = subject.original_width(100).original_height(100).width(50).height(50).center(50, 0).generate expect(url).to eq '/trIjfr513nkGkCpKXK6qgox2jPA=/50x50/my.domain.com/some/image/url.jpg' end - it "should blow up with a bad center" do - expect { subject.original_width(100).original_height(100).width(50).height(50).center(50).generate }.to raise_error(RuntimeError) + it 'should blow up with a bad center' do + expect do + subject.original_width(100).original_height(100).width(50).height(50).center(50).generate + end.to raise_error(RuntimeError) end - it "should have no crop with a missing original_height" do + it 'should have no crop with a missing original_height' do url = subject.original_width(100).width(50).height(40).center(50, 0).generate expect(url).to eq '/veYlY0msKmemAaXpeav2kCNftmU=/50x40/my.domain.com/some/image/url.jpg' end - it "should have no crop with a missing original_width" do + it 'should have no crop with a missing original_width' do url = subject.original_height(100).width(50).height(40).center(50, 0).generate expect(url).to eq '/veYlY0msKmemAaXpeav2kCNftmU=/50x40/my.domain.com/some/image/url.jpg' end - it "should have no crop with out a width and height" do + it 'should have no crop with out a width and height' do url = subject.original_width(100).original_height(100).center(50, 50).generate expect(url).to eq '/964rCTkAEDtvjy_a572k7kRa0SU=/my.domain.com/some/image/url.jpg' end - it "should use the original width with a missing width" do + it 'should use the original width with a missing width' do url = subject.original_width(100).original_height(100).height(80).center(50, 50).generate expect(url).to eq '/02BNIIJ9NYNV9Q03JHPtlP0DIDg=/0x10:100x90/0x80/my.domain.com/some/image/url.jpg' end - it "should use the original height with a missing height" do + it 'should use the original height with a missing height' do url = subject.original_width(100).original_height(100).width(80).center(50, 50).generate expect(url).to eq '/0XL5BmMi3vlJQfw6aGOVW-M1vVI=/10x0:90x100/80x0/my.domain.com/some/image/url.jpg' end - it "should have the right crop with a negative width" do + it 'should have the right crop with a negative width' do url = subject.original_width(100).original_height(100).width(-50).height(40).center(50, 50).generate expect(url).to eq '/IuRNPlFlpTVol45bDkOm2PGvneo=/0x10:100x90/-50x40/my.domain.com/some/image/url.jpg' end - it "should have the right crop with a negative height" do + it 'should have the right crop with a negative height' do url = subject.original_width(100).original_height(100).width(50).height(-40).center(50, 50).generate expect(url).to eq '/-8IhWGEeXaY1uv945i9EHLVjwuk=/0x10:100x90/50x-40/my.domain.com/some/image/url.jpg' end - it "should have the right crop with a negative height and width" do + it 'should have the right crop with a negative height and width' do url = subject.original_width(100).original_height(100).width(-50).height(-40).center(50, 50).generate expect(url).to eq '/lfjGLTTEaW_Rcvc1q0ZhfYup2jg=/0x10:100x90/-50x-40/my.domain.com/some/image/url.jpg' end diff --git a/tasks/build.rake b/tasks/build.rake index 04dd2f1..feec830 100644 --- a/tasks/build.rake +++ b/tasks/build.rake @@ -1,15 +1,17 @@ -lib = File.expand_path('../../lib/', __FILE__) -$:.unshift lib unless $:.include?(lib) +# frozen_string_literal: true + +lib = File.expand_path('../lib', __dir__) +$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) require 'thumbor/version' -desc "Build gem last version" +desc 'Build gem last version' task :build do system 'mkdir -p pkg/gems' - system "gem build ruby-thumbor.gemspec" + system 'gem build ruby-thumbor.gemspec' system "mv ruby-thumbor-#{Thumbor::VERSION}.gem pkg/" end -desc "Build and gem upload" -task :release => :build do +desc 'Build and gem upload' +task release: :build do system "gem push pkg/ruby-thumbor-#{Thumbor::VERSION}.gem" -end \ No newline at end of file +end diff --git a/tasks/rspec.rake b/tasks/rspec.rake index 63a721d..6ab40cf 100644 --- a/tasks/rspec.rake +++ b/tasks/rspec.rake @@ -1,3 +1,5 @@ +# frozen_string_literal: true + begin require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) From cde949f3b15b7ab15e32e9aa3ac5ebf6187fb509 Mon Sep 17 00:00:00 2001 From: Guilherme Souza <101073+guilhermef@users.noreply.github.com> Date: Fri, 28 Jan 2022 02:02:29 +0100 Subject: [PATCH 3/6] Add rubocop --- .github/workflows/main.yml | 13 +++++++++++++ .rubocop.yml | 15 +++++++++++++++ Gemfile | 3 --- lib/thumbor/crypto_url.rb | 6 ------ ruby-thumbor.gemspec | 3 +++ spec/thumbor/cascade_spec.rb | 6 ------ tasks/rspec.rake | 7 ++----- 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5867f6..3e44cd0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,3 +37,16 @@ jobs: with: github-token: ${{ secrets.github_token }} parallel-finished: true + rubocop: + name: runner / rubocop + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1 + - name: rubocop + uses: reviewdog/action-rubocop@v2 + with: + rubocop_version: gemfile + reporter: github-pr-check diff --git a/.rubocop.yml b/.rubocop.yml index fc18d2d..d4a13bc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,6 +4,9 @@ Naming/FileName: Style/Documentation: Enabled: false +Gemspec/RequiredRubyVersion: + Enabled: false + Metrics/ClassLength: Max: 130 @@ -12,3 +15,15 @@ Layout/LineLength: Metrics/BlockLength: Max: 255 + +Metrics/AbcSize: + Max: 74 + +Metrics/CyclomaticComplexity: + Max: 26 + +Metrics/MethodLength: + Max: 44 + +Metrics/PerceivedComplexity: + Max: 26 diff --git a/Gemfile b/Gemfile index 0caa3fc..7f4f5e9 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,3 @@ source 'https://rubygems.org' gemspec - -gem 'simplecov', require: false -gem 'simplecov-lcov', require: false diff --git a/lib/thumbor/crypto_url.rb b/lib/thumbor/crypto_url.rb index a5a11b4..7cfa04b 100644 --- a/lib/thumbor/crypto_url.rb +++ b/lib/thumbor/crypto_url.rb @@ -7,16 +7,10 @@ module Thumbor class CryptoURL - attr_writer :computed_key - def initialize(key = nil) @key = key end - def computed_key - (@key * 16)[0..15] - end - def calculate_width_and_height(url_parts, options) width = options[:width] height = options[:height] diff --git a/ruby-thumbor.gemspec b/ruby-thumbor.gemspec index 138747b..e379195 100644 --- a/ruby-thumbor.gemspec +++ b/ruby-thumbor.gemspec @@ -36,4 +36,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'rake', '~> 13.0' s.add_development_dependency 'rspec', '~> 3.10' + s.add_development_dependency 'rubocop-rspec', '~> 2.8.0' + s.add_development_dependency 'simplecov', '~> 0.21.2' + s.add_development_dependency 'simplecov-lcov', '~> 0.8.0' end diff --git a/spec/thumbor/cascade_spec.rb b/spec/thumbor/cascade_spec.rb index 43fa4cc..a037c14 100644 --- a/spec/thumbor/cascade_spec.rb +++ b/spec/thumbor/cascade_spec.rb @@ -10,12 +10,6 @@ subject { Thumbor::Cascade.new key, image_url } - describe '#new' do - it 'should create a new instance passing key and keep it' do - expect(subject.computed_key).to eq 'my-security-keym' - end - end - it 'should raise an error' do expect { subject.god_of_war_crop }.to raise_error(NoMethodError) end diff --git a/tasks/rspec.rake b/tasks/rspec.rake index 6ab40cf..b263959 100644 --- a/tasks/rspec.rake +++ b/tasks/rspec.rake @@ -1,7 +1,4 @@ # frozen_string_literal: true -begin - require 'rspec/core/rake_task' - RSpec::Core::RakeTask.new(:spec) -rescue LoadError -end +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) From 0477ea8d83c47cef1ba214884ee83d4c43f31d89 Mon Sep 17 00:00:00 2001 From: Guilherme Souza <101073+guilhermef@users.noreply.github.com> Date: Fri, 28 Jan 2022 02:11:21 +0100 Subject: [PATCH 4/6] Fix rake --- Rakefile | 12 +++++++++++- tasks/build.rake | 17 ----------------- tasks/rspec.rake | 4 ---- 3 files changed, 11 insertions(+), 22 deletions(-) delete mode 100644 tasks/build.rake delete mode 100644 tasks/rspec.rake diff --git a/Rakefile b/Rakefile index e386165..ae53316 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,15 @@ # frozen_string_literal: true -Dir.glob('tasks/*.rake').each { |r| import r } +require 'rspec/core/rake_task' + +begin + require 'bundler/setup' +rescue LoadError + puts 'You must `gem install bundler` and `bundle install` to run rake tasks' +end + +Bundler::GemHelper.install_tasks + +RSpec::Core::RakeTask.new(:spec) task default: :spec diff --git a/tasks/build.rake b/tasks/build.rake deleted file mode 100644 index feec830..0000000 --- a/tasks/build.rake +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -lib = File.expand_path('../lib', __dir__) -$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) -require 'thumbor/version' - -desc 'Build gem last version' -task :build do - system 'mkdir -p pkg/gems' - system 'gem build ruby-thumbor.gemspec' - system "mv ruby-thumbor-#{Thumbor::VERSION}.gem pkg/" -end - -desc 'Build and gem upload' -task release: :build do - system "gem push pkg/ruby-thumbor-#{Thumbor::VERSION}.gem" -end diff --git a/tasks/rspec.rake b/tasks/rspec.rake deleted file mode 100644 index b263959..0000000 --- a/tasks/rspec.rake +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -require 'rspec/core/rake_task' -RSpec::Core::RakeTask.new(:spec) From 9c815a507fd725edb11c5c96b26f71bca225ffbb Mon Sep 17 00:00:00 2001 From: Guilherme Souza <101073+guilhermef@users.noreply.github.com> Date: Fri, 28 Jan 2022 02:29:31 +0100 Subject: [PATCH 5/6] Add release file --- .github/workflows/release.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5de9d4d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,22 @@ +name: Ruby + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + release: + name: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1 + - name: Install dependencies + run: bundle install + - name: push to RubyGems + env: + GEM_HOST_API_KEY: ${{ secrets.GEM_HOST_API_KEY }} + run: bundle exec rake release From 9b5f469ce106de0449b5e1f4a0dd3143d003612d Mon Sep 17 00:00:00 2001 From: Guilherme Souza <101073+guilhermef@users.noreply.github.com> Date: Fri, 28 Jan 2022 02:34:33 +0100 Subject: [PATCH 6/6] Bump to 4.0.0 --- lib/thumbor/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/thumbor/version.rb b/lib/thumbor/version.rb index c1b1ed7..c871132 100644 --- a/lib/thumbor/version.rb +++ b/lib/thumbor/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Thumbor - VERSION = '3.0.0' + VERSION = '4.0.0' end