RSpec be_equal not working - ruby-on-rails

I am writing some rspec examples for a plain old ruby class in my rails project and I am facing the following problem.
I have this constructor:
class Server
def initialize(host='localhost',options={:port => 443, :password => '', :vpncmd_bin_path => '/usr/local/bin/vpncmd', :timeout => 5})
#host = host
#port = options[:port].present? ? options[:port] : 443
#password = options[:password].present? ? options[:password] : ''
#vpncmd_bin_path = options[:vpncmd_bin_path].present? ? options[:vpncmd_bin_path] : '/usr/local/bin/vpncmd'
#timeout = options[:timeout].present? ? options[:timeout] : 5
#hubs = {}
#hub_cache_dirty = true
#hub_password_cache = {}
end
...
end
This test example:
it "should have a default constructor that takes no argument" do
s = SoftEther::Server.new()
expect(s.host).to be_equal('localhost')
expect(s.port).to be_equal('443')
expect(s.timeout).to be_equal(5)
expect(s.vpncmd_bin_path).to be_equal('/usr/local/bin/vpncmd')
expect(s.password).to be_equal('')
end
And rspec gives me the following result with Rails 4.2.6, jruby-9.0.5.0 and 3.4.4:
1) SoftEtherSever should have a default constructor that takes no argument
Failure/Error: expect(s.host).to be_equal('localhost')
expected `"localhost".equal?("localhost")` to return true, got false
# ./spec/poro/softether_spec.rb:19:in `block in (root)'
What did I do wrong?

equal? checks whether two instances are the same. But it returns false when two strings contains the same value but refers to different objects:
"foo".equals?("foo")
# => false
What you should really use is eq()
expect(s.host).to eq('localhost')

Just to add an edge case to Simone's answer:
If you were to freeze the strings in question, you would get the result you expected:
irb(main):001:0> 'test'.equal? 'test'
=> false
irb(main):002:0> 'test'.freeze.equal? 'test'.freeze
=> true
In Ruby 2.3, this can be done by adding
# frozen_string_literal: true
to the top of the Ruby file.
With that said, Simone is right. You should use the eq matcher unless you truly want to test that you are using the same exact object instance. Then using equal is in order.

Related

With Rails 4.2 and lograge, how do I enable date/times before each logged line?

I use the gem “Lograge” 0.3.6, and Rails 4.2. I have this configured in my config/environments/development.rb file
config.lograge.enabled = true
config.lograge.formatter = CustomLogstash.new
However, I notice the output in my log/development.log file doesn’t contain date/times in front of each line. How do I configure lograge (or maybe just my Rails logger?) to prefix each line in that file with a date and time?
As per the document, the lograge gem provides below log formatters.
Lograge::Formatters::Lines.new # need to install "lines" gem
Lograge::Formatters::Cee.new
Lograge::Formatters::Graylog2.new
Lograge::Formatters::KeyValue.new # default lograge format
Lograge::Formatters::Json.new
Lograge::Formatters::Logstash.new # need to install "logstash-event" gem
Lograge::Formatters::LTSV.new
Lograge::Formatters::Raw.new # Returns a ruby hash object
By default the lograge gem uses Lograge::Formatters::KeyValue.new format for log.
You can customize this and make it universal by using your CustomLogStash class with some changes.
class CustomLogStash < Lograge::Formatters::KeyValue
def call(data)
# I'm using timestamp key here, you can choose whatever you want.
data_hash = { timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3N")}.merge!(data)
super(data_hash)
end
end
Same way you can use any Lograge::Formatters class and apply the custom format to the log.
Now add below code to your config/initializers/lograge.rb file.
Rails.application.configure do
config.lograge.enabled = true
config.lograge.formatter = CustomLogStash.new
end
Now restart your server and load a page in your browser. You will see the logs something like below:
timestamp=2021-11-21T17:14:10.726 method=GET path=/categories format=html controller=categories action=index status=200 duration=259.11 view=244.91 db=2.60
EDITED
If you are looking for logs something like
then you don't need any gem for this. You can achieve this by adding below lines to your preferred environment development/production/test
config.log_level = :debug
config.log_formatter = ::Logger::Formatter.new
If you want to apply this across all environments then add above lines to the config/application.rb file.
Let me know if it helps.
Can you add the following in config if it helps.
config.lograge.formatter = ->(data) { data.reverse_merge({time: Time.now}) }# data is a ruby hash.
It will give you output like following
{:time=>2021-11-16 12:26:24.65362 +0000, :method=>"GET", :path=>"/", :format=>:html, :controller=>"Controller", :action=>"index", :status=>200, :duration=>393.41, :view=>85.55, :db=>1.38}
Can you paste contents of CustomLogstash class? From docs, this class should respond to call method and return Hash.
This works for me:
class CustomLogstash
def call(data)
{ time: Time.now, controller: data[:controller] } # this can be anything as long it is Hash, eg. data.merge(time: Time.now)
end
end
Sample output from above:
{:time=>"2021-11-18T20:31:41.486+01:00", :controller=>"calendar_events"}
As per official documentation for lograge, you can make use of custom_options
EDIT 1 : custom_options using time: Time.now or time:event.time
Rails.application.configure do
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Logstash.new
# add time to lograge
config.lograge.custom_options = lambda do |event|
{ time: Time.now } #or use time:event.time
end
end
Note: When using the logstash output, you need to add the additional gem logstash-event. You can simply add it to your Gemfile like this
gem "logstash-event"
EDIT 2: Update based on comments custom_options using :time => event.time
#config/environments/production.rb
MyApp::Application.configure do
config.lograge.enabled = true
# add time to lograge
config.lograge.custom_options = lambda do |event|
{:time => event.time}
end
end
OR the below custom options which was a fix in lograge issue to ensure both date and time logged using time: event.time.to_s(:db)
config.lograge.custom_options = lambda do |event|
unwanted_keys = %w[format action controller utf8]
params = event.payload[:params].reject { |key,_| unwanted_keys.include? key }
{time: event.time.to_s(:db), user: event.payload[:user], params: params}
end
ALTERNATIVELY you can use this Custom logger
# Define a setter to pass in a custom log formatter
class ActiveSupport::BufferedLogger
def formatter=(formatter)
#log.formatter = formatter
end
end
# Defines a custom log format (time, severity, message, PID, backtrace)... all with color!
class Formatter
SEVERITY_TO_TAG = {'DEBUG'=>'meh', 'INFO'=>'fyi', 'WARN'=>'hmm', 'ERROR'=>'wtf', 'FATAL'=>'omg', 'UNKNOWN'=>'???'}
SEVERITY_TO_COLOR = {'DEBUG'=>'37', 'INFO'=>'32', 'WARN'=>'33', 'ERROR'=>'31', 'FATAL'=>'31', 'UNKNOWN'=>'37'}
HUMOR_FOR_ENV = {development: true, test: true, production: false}
DEPTH_FOR_ENV = {development: 3, test: 3, production: 1}
EXCLUSION_REGEX = /log|active_support|active_record/
def humorous?
return #is_humorous if defined? #is_humorous
#is_humorous = HUMOR_FOR_ENV[ Rails.env.to_sym ]
end
def depth
#depth ||= DEPTH_FOR_ENV[ Rails.env.to_sym ]
end
def call(severity, time, progname, msg)
t = time.strftime("%Y-%m-%d %H:%M:%S.") << time.usec.to_s[0..2].rjust(3)
color = SEVERITY_TO_COLOR[severity]
sev = humorous? ? "%-3s" % SEVERITY_TO_TAG[severity] # pad to at least 3 characters
: "%-5s" % severity # pad to at least 5 characters
# 2013-05-01 19:16:00.785 [omg] oh noes! (pid:30976) (admin/user.rb:45:in `block (4 levels) in <top (required)>') <- `call' <- `content_for' <- `block (2 levels) in row' <- `block in build_tag'
"\033[0;37m#{t}\033[0m [\033[#{color}m#{sev}\033[0m] #{msg.strip} (pid:#{$$}) #{whodunit}\033[0m\n"
end
def whodunit
latest, *others = caller.select{ |a| a !~ EXCLUSION_REGEX }[0, depth]
latest = latest[/(lib|app)\/(.*)/,-1] || latest
string = ""
string << "\033[36m(#{latest})"
string << "\033[35m <- " + others.map{ |s| s[/`.*/] }.join(' <- ') if others.any?
string
end
end
Rails.logger.formatter = Formatter.new
For Rails 4.2 don’t forget to add ActiveSupport::TaggedLogging to be able to call custom logger like a default rails logger
ActiveSupport::TaggedLogging is used to wrap any standard logger instance to add "tags" to a log statement. A "tag" in this case usually describes a subdomain, and is used by the default Rails.logger to allow you to tag log statements with subdomains, request ids, etc. in your multi-user, multi-instance production applications.
include
ActiveSupport::TaggedLogging::Formatter

to the Formatter class.

Geocoder::Result::Geoip2#country returns nil even though location.data['country']['names']['en'] is not

I can't get Ahoy to work with Geocoder and the problem is related to this Ahoy line:
module Ahoy
class GeocodeV2Job < ActiveJob::Base
[...]
if location && location.country.present? # <= but my location.country is never present...
This is really odd because, from my console, I get this:
> heroku run rails c
> visit = Ahoy::Visit.last
> location = Geocoder.search(visit.ip).first
=> #<Geocoder::Result::Geoip2:0x00000006e14910 #data=#<MaxMindDB::Result:0x00000006e150b8 #raw={"city"=>{"geoname_id"=>3170918, "names"=>{"de"=>"Piasco", "en"=>"Piasco", "fr"=>"Piasco", "pt-BR"=>"Piasco"}}, "continent"=>{"code"=>"EU", "geoname_id"=>6255148, "names"=>{"de"=>"Europa", "en"=>"Europe", "es"=>"Europa", "fr"=>"Europe", "ja"=>"ヨーロッパ", "pt-BR"=>"Europa", "ru"=>"Европа", "zh-CN"=>"欧洲"}}, "country"=>{"geoname_id"=>3175395, "is_in_european_union"=>true, "iso_code"=>"IT", "names"=>{"de"=>"Italien", "en"=>"Italy", "es"=>"Italia", "fr"=>"Italie", "ja"=>"イタリア共和国", "pt-BR"=>"Itália", "ru"=>"Италия", "zh-CN"=>"意大利"}}, "location"=>{"accuracy_radius"=>50, "latitude"=>44.5611, "longitude"=>7.4442, "time_zone"=>"Europe/Rome"}, "postal"=>{"code"=>"12026"}, "registered_country"=>{"geoname_id"=>3175395, "is_in_european_union"=>true, "iso_code"=>"IT", "names"=>{"de"=>"Italien", "en"=>"Italy", "es"=>"Italia", "fr"=>"Italie", "ja"=>"イタリア共和国", "pt-BR"=>"Itália", "ru"=>"Италия", "zh-CN"=>"意大利"}}, "subdivisions"=>[{"geoname_id"=>3170831, "iso_code"=>"21", "names"=>{"de"=>"Piemont", "en"=>"Piedmont", "es"=>"Piamonte", "fr"=>"Piémont", "ja"=>"ピエモンテ州", "ru"=>"Пьемонт", "zh-CN"=>"皮埃蒙特"}}, {"geoname_id"=>3177699, "iso_code"=>"CN", "names"=>{"en"=>"Provincia di Cuneo", "fr"=>"Coni"}}], "network"=>"85.159.180.0/23"}>, #cache_hit=nil>
So location is not nil!
But then:
> location.country
=> ""
Even though: 👀
irb(main):008:0> location.data['country']['names']['en']
=> "Italy"
Please note that location is a:
> location.class
=> Geocoder::Result::Geoip2
I'm riding:
> Rails.version
=> "4.2.8"
> Ahoy::VERSION
=> "2.2.0"
Using geocoder 1.5.0.
And these are my initializers:
module Ahoy
class Store < Ahoy::DatabaseStore
def track_visit(data)
data[:ip] = request.headers['CF-Connecting-IP'] if request.headers['CF-Connecting-IP'].present? # Because I'm using Cloudflare
super(data)
end
def track_event(data)
data[:properties][:subdomain] = request.subdomain
data[:properties][:domain] = request.domain
super(data)
end
end
end
Ahoy.api = true
Ahoy.geocode = true
Geocoder.configure(
lookup: :location_iq,
api_key: ENV['LOCATION_IQ_ACCESS_TKN'],
language: 'it',
cache: Rails.cache,
ip_lookup: :geoip2,
geoip2: {
file: Rails.root.join('lib', 'geocoder', 'GeoLite2-City.mmdb')
}
)
What am I missing?
Thank you!
Ok, I figured this out. The problem, as usual, is my homeland ;-)
In other words... in my geocoder initializer I set 'it' as the default language but the GeoLite2-City.mmdb database, embedded in the gem 'maxminddb', doesn't support Italian.
So I just have to figure out how to make just the ip_lookup work in English and it all be fine :)

undefined method: connect_timeout

Browser error:
NoMethodError
undefined method `connect_timeout=' for #<Mysql2::Client:0x47f7570>
On my browser, an error comes up that connect_timeout is undefined. I'm pretty sure it has something to do with the client.rb file. I'll show you the file. I had to edit some of it to actually get Webrick up and running. When I started the server, an error always appeared on my command line unless I made the changes. I've commented on what I have edited. Sometimes edited random things and some of them worked but they produced different errors on my browser. I am using a windows 8 machine. Thank you for helping.
module Mysql2
class Client
attr_reader :query_options, :read_timeout
##default_query_options = {
:as => :hash, # the type of object you want each row back as; also supports :array (an array of values)
:async => false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result
:cast_booleans => false, # cast tinyint(1) fields as true/false in ruby
:symbolize_keys => false, # return field names as symbols instead of strings
:database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in
:application_timezone => nil, # timezone Mysql2 will convert to before handing the object back to the caller
:cache_rows => true, # tells Mysql2 to use it's internal row cache for results
#:connect_flags => REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION,
#I had to delete the line above because for some reason the command prompt said that each of the constants were undefined were not used in the right place or something
:cast => true,
:default_file => nil,
:default_group => nil
}
def initialize (opts = {})
opts = Mysql2::Util.key_hash_as_symbols( opts )
#read_timeout = nil
#query_options = ##default_query_options.dup
#query_options.merge! opts
#initialize_ext
# the chrome page said that the above variable is undefined :P
# Set default connect_timeout to avoid unlimited retries from signal interruption
opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout)
[:reconnect, :connect_timeout, :local_infile, :read_timeout, :write_timeout, :default_file, :default_group, :secure_auth, :init_command].each do |key|
next unless opts.key?(key)
case key
when :reconnect, :local_infile, :secure_auth
send(:"#{key}=", !!opts[key])
when :connect_timeout, :read_timeout, :write_timeout
send(:"#{key}=", opts[key].to_i)
else
send(:"#{key}=", opts[key])
end
end
# force the encoding to utf8
self.charset_name = opts[:encoding] || 'utf8'
ssl_options = opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher)
ssl_set(*ssl_options) if ssl_options.any?
if [:user,:pass,:hostname,:dbname,:db,:sock].any?{|k| #query_options.has_key?(k) }
warn "============= WARNING FROM mysql2 ============="
warn "The options :user, :pass, :hostname, :dbname, :db, and :sock will be deprecated at some point in the future."
warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options."
warn "============= END WARNING FROM mysql2 ========="
end
user = opts[:username] || opts[:user]
pass = opts[:password] || opts[:pass]
host = opts[:host] || opts[:hostname]
port = opts[:port]
database = opts[:database] || opts[:dbname] || opts[:db]
socket = opts[:socket] || opts[:sock]
flags = opts[:flags] ? opts[:flags] | #query_options[:connect_flags] : #query_options[:connect_flags]
# Correct the data types before passing these values down to the C level
user = user.to_s unless user.nil?
pass = pass.to_s unless pass.nil?
host = host.to_s unless host.nil?
port = port.to_i unless port.nil?
database = database.to_s unless database.nil?
socket = socket.to_s unless socket.nil?
connect user, pass, host, port, database, socket, flags
end
def self.default_query_options
##default_query_options
end
def query_info
info = query_info_string
return {} unless info
info_hash = {}
info.split.each_slice(2) { |s| info_hash[s[0].downcase.delete(':').to_sym] = s[1].to_i }
info_hash
end
private
def self.local_offset
::Time.local(2010).utc_offset.to_r / 86400
end
end
end
Mysql2::Client#initialize called connect_timeout= but there isn't such attr_writer in the client.
when :connect_timeout, :read_timeout, :write_timeout
send(:"#{key}=", opts[key].to_i)
else
If this client is written by yourself, add attr_accessor :connect_timeout in Mysql2::Client's definition and make proper use of the attribute. If it is from other library, check your load path. You may have missed some files that opened Mysql2::Client and monkey patched it.

I18n: How to check if a translation key/value pairs is missing?

I am using Ruby on Rails 3.1.0 and the I18n gem. I (am implementing a plugin and) I would like to check at runtime if the I18n is missing a translation key/value pairs and, if so, to use a custom string. That is, I have:
validates :link_url,
:format => {
:with => REGEX,
:message => I18n.t(
'custom_invalid_format',
:scope => 'activerecord.errors.messages'
)
}
If in the .yml file there is not the following code
activerecord:
errors:
messages:
custom_invalid_format: This is the test error message 1
I would like to use the This is the test error message 2. Is it possible? If so, how can I make that?
BTW: For performance reasons, is it advisable to check at runtime if the translation key/value pairs is present?
You could pass a :default parameter to I18n.t:
I18n.t :missing, :default => 'Not here'
# => 'Not here'
You can read more about it here.
I just had the same question and I want to compute an automatic string in case the translation is missing. If I use the :default option I have to compute the automatic string every time even when the translation is not missing. So I searched for another solution.
You can add the option :raise => true or use I18n.translate! instead of I18n.translate. If no translation can be found an exception is raised.
begin
I18n.translate!('this.key.should.be.translated', :raise => true)
rescue I18n::MissingTranslationData
do_some_resource_eating_text_generation_here
end
I don't know how to this at runtime but you can use rake to find it out. You'll have create your own rake task for that. Here's one:
namespace :i18n do
desc "Find and list translation keys that do not exist in all locales"
task :missing_keys => :environment do
def collect_keys(scope, translations)
full_keys = []
translations.to_a.each do |key, translations|
new_scope = scope.dup << key
if translations.is_a?(Hash)
full_keys += collect_keys(new_scope, translations)
else
full_keys << new_scope.join('.')
end
end
return full_keys
end
# Make sure we've loaded the translations
I18n.backend.send(:init_translations)
puts "#{I18n.available_locales.size} #{I18n.available_locales.size == 1 ? 'locale' : 'locales'} available: #{I18n.available_locales.to_sentence}"
# Get all keys from all locales
all_keys = I18n.backend.send(:translations).collect do |check_locale, translations|
collect_keys([], translations).sort
end.flatten.uniq
puts "#{all_keys.size} #{all_keys.size == 1 ? 'unique key' : 'unique keys'} found."
missing_keys = {}
all_keys.each do |key|
I18n.available_locales.each do |locale|
I18n.locale = locale
begin
result = I18n.translate(key, :raise => true)
rescue I18n::MissingInterpolationArgument
# noop
rescue I18n::MissingTranslationData
if missing_keys[key]
missing_keys[key] << locale
else
missing_keys[key] = [locale]
end
end
end
end
puts "#{missing_keys.size} #{missing_keys.size == 1 ? 'key is missing' : 'keys are missing'} from one or more locales:"
missing_keys.keys.sort.each do |key|
puts "'#{key}': Missing from #{missing_keys[key].join(', ')}"
end
end
end
put the given in a .rake file in your lib/tasks directory and execute:
rake i18n:missing_keys
Information source is here and code on github here.
If you wish to pass variable to the message like This is the test error message {variable}
This is possible using variable in language file like below.
# app/views/home/index.html.erb
<%=t 'greet_username', :user => "Bill", :message => "Goodbye" %>
# config/locales/en.yml
en:
greet_username: "%{message}, %{user}!"
More description you can find here.

How do I check whether a value in a string is an IP address

when I do this
ip = request.env["REMOTE_ADDR"]
I get the client's IP address it it. But what if I want to validate whether the value in the variable is really an IP?
How do I do that?
Please help.
Thanks in advance. And sorry if this question is repeated, I didn't take the effort of finding it...
EDIT
What about IPv6 IP's??
Ruby has already the needed Regex in the standard library.
Checkout resolv.
require "resolv"
"192.168.1.1" =~ Resolv::IPv4::Regex ? true : false #=> true
"192.168.1.500" =~ Resolv::IPv4::Regex ? true : false #=> false
"ff02::1" =~ Resolv::IPv6::Regex ? true : false #=> true
"ff02::1::1" =~ Resolv::IPv6::Regex ? true : false #=> false
If you like it the short way ...
require "resolv"
!!("192.168.1.1" =~ Resolv::IPv4::Regex) #=> true
!!("192.168.1.500" =~ Resolv::IPv4::Regex) #=> false
!!("ff02::1" =~ Resolv::IPv6::Regex) #=> true
!!("ff02::1::1" =~ Resolv::IPv6::Regex) #=> false
Have fun!
Update (2018-10-08):
From the comments below i love the very short version:
!!(ip_string =~ Regexp.union([Resolv::IPv4::Regex, Resolv::IPv6::Regex]))
Very elegant with rails (also an answer from below):
validates :ip,
:format => {
:with => Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex)
}
Why not let a library validate it for you? You shouldn't introduce complex regular expressions that are impossible to maintain.
% gem install ipaddress
Then, in your application
require "ipaddress"
IPAddress.valid? "192.128.0.12"
#=> true
IPAddress.valid? "192.128.0.260"
#=> false
# Validate IPv6 addresses without additional work.
IPAddress.valid? "ff02::1"
#=> true
IPAddress.valid? "ff02::ff::1"
#=> false
IPAddress.valid_ipv4? "192.128.0.12"
#=> true
IPAddress.valid_ipv6? "192.128.0.12"
#=> false
You can also use Ruby's built-in IPAddr class, but it doesn't lend itself very well for validation.
Of course, if the IP address is supplied to you by the application server or framework, there is no reason to validate at all. Simply use the information that is given to you, and handle any exceptions gracefully.
require 'ipaddr'
!(IPAddr.new(str) rescue nil).nil?
I use it for quick check because it uses built in library. Supports both ipv4 and ipv6. It is not very strict though, it says '999.999.999.999' is valid, for example. See the winning answer if you need more precision.
As most of the answers don't speak about IPV6 validation, I had the similar problem.
I solved it by using the Ruby Regex Library, as #wingfire mentionned it.
But I also used the Regexp Library to use it's union method as explained here
I so have this code for a validation :
validates :ip, :format => {
:with => Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex)
}
Hope this can help someone !
Use http://www.ruby-doc.org/stdlib-1.9.3/libdoc/ipaddr/rdoc/IPAddr.html it performs validation for you. Just rescue the exception with false and you know that it was invalid.
1.9.3p194 :002 > IPAddr.new('1.2.3.4')
=> #<IPAddr: IPv4:1.2.3.4/255.255.255.255>
1.9.3p194 :003 > IPAddr.new('1.2.3.a')
ArgumentError: invalid address
from /usr/local/rvm/rubies/ruby-1.9.3-p194/lib/ruby/1.9.1/ipaddr.rb:496:in `rescue in initialize'
from /usr/local/rvm/rubies/ruby-1.9.3-p194/lib/ruby/1.9.1/ipaddr.rb:493:in `initialize'
from (irb):3:in `new'
from (irb):3
from /usr/local/rvm/rubies/ruby-1.9.3-p194/bin/irb:16:in `<main>'
require 'ipaddr'
def is_ip?(ip)
!!IPAddr.new(ip) rescue false
end
is_ip?("192.168.0.1")
=> true
is_ip?("www.google.com")
=> false
Or, if you don't mind extending core classes:
require 'ipaddr'
class String
def is_ip?
!!IPAddr.new(self) rescue false
end
end
"192.168.0.1".is_ip?
=> true
"192.168.0.512".is_ip?
=> false
All answers above asume IPv4... you must ask yourself how wise it is to limit you app to IPv4 by adding these kind of checks in this day of the net migrating to IPv6.
If you ask me: Don't validate it at all. Instead just pass the string as-is to the network components that will be using the IP address and let them do the validation. Catch the exceptions they will throw when it is wrong and use that information to tell the user what happened. Don't re-invent the wheel, build upon the work of others.
Try this
Use IPAddr
require 'ipaddr'
true if IPAddr.new(ip) rescue false
This regular expression I use which I found here
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
IP address in a string form must contain exactly four numbers, separated by dots. Each number must be in a range between 0 and 255, inclusive.
Validate using regular expression:
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
for match a valid IP adress with regexp use
^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$
instead of
^([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])(\.([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])){3}$
because many regex engine match the first possibility in the OR sequence
you can try your regex engine : 10.48.0.200
test the difference here
for ipv4
def ipv4?(str)
nums = str.split('.')
reg = /^\d$|^[1-9]\d$|^1\d\d$|^2[0-4]\d$|^25[0-5]$/
nums.length == 4 && (nums.count {|n| reg.match?(n)}) == 4
end

Resources