diff --git a/.github/matrix.json b/.github/matrix.json index 81e37ac5f94b..f15a18dd6920 100644 --- a/.github/matrix.json +++ b/.github/matrix.json @@ -1,5 +1,5 @@ { "postgresql": ["12"], - "ruby": ["2.7"], + "ruby": ["2.7", "3.0"], "node": ["14"] } diff --git a/app/models/concerns/foreman/sti.rb b/app/models/concerns/foreman/sti.rb index 79a59f418f5f..c1926215f869 100644 --- a/app/models/concerns/foreman/sti.rb +++ b/app/models/concerns/foreman/sti.rb @@ -8,11 +8,11 @@ class << base module ClassMethods # ensures that the correct STI object is created when :type is passed. - def new(*attributes, &block) - if (h = attributes.first).is_a?(Hash) && (type = h.with_indifferent_access.delete(:type)) && !type.empty? + def new(attributes = nil, &block) + if attributes.is_a?(Hash) && (type = attributes.with_indifferent_access.delete(:type)) && !type.empty? if (klass = type.constantize) != self raise "Invalid type #{type}" unless klass <= self - return klass.new(*attributes, &block) + return klass.new(attributes, &block) end end diff --git a/app/models/host.rb b/app/models/host.rb index 12d7d98593ee..f609a5bf1d9e 100644 --- a/app/models/host.rb +++ b/app/models/host.rb @@ -1,8 +1,9 @@ module Host - def self.method_missing(method, *args, &block) + def self.method_missing(method, *args, **kwargs, &block) type = "Host::Managed" case method.to_s when /create/, 'new' + # in this case args should contain a hash with host attributes if args.empty? || args[0].nil? # got no parameters # set the default type args = [{:type => type}] @@ -10,11 +11,41 @@ def self.method_missing(method, *args, &block) args[0][:type] ||= type # adds the type if it doesn't exists type = args[0][:type] # stores the type for later usage. end - end - if type.constantize.respond_to?(method, true) - type.constantize.send(method, *args, &block) + type.constantize.send(method, kwargs.merge(args.first), &block) # quick skip for simple cases else - super + klass = type.constantize + if klass.respond_to?(method, true) + # Removing block, since we will pass it anyway + meth_params = klass.method(method).parameters.collect { |par_desc| par_desc.first } - [:block] + if meth_params.empty? || (args.empty? && kwargs.empty?) + klass.send(method, &block) + elsif meth_params == [:rest] + # means that the method could accept anything, e.g. def find_by(*args), + # but internally would expect a Hash wrapped by *args array + # or there are cases like Array#last, which has * as param list, but expects an Integer + # since there is a lot of delegation in Rails, it's hard to know exact signature of the real method: + # find_in_batches expects only kwargs, but method(:find_in_batces) returns (*) as param list + # through the same delegation goes find_by with a different signature/expectations + + if !args.empty? + klass.send(method, *args, &block) + elsif kwargs.any? + klass.send(method, **kwargs, &block) + # here should probably be a closing "else" for other cases + end + elsif (meth_params & [:req, :opt, :rest]).empty? + # this would mean we pass kwargs only + klass.send(method, **kwargs, &block) + elsif (meth_params & [:key, :keyreq, :keyrest]).empty? + # if there is no kwargs, let's treat this as before + klass.send(method, *args, &block) + else + # let's treat as we should + klass.send(method, *args, **kwargs, &block) + end + else + super + end end end diff --git a/app/models/host/base.rb b/app/models/host/base.rb index 91cec5dadc30..d19968705fa2 100644 --- a/app/models/host/base.rb +++ b/app/models/host/base.rb @@ -90,11 +90,12 @@ def self.taxonomy_conditions # initializer and we set name when we are sure that we have primary interface # we can't create primary interface before calling super because args may contain nested # interface attributes - def initialize(*args) + def initialize(attributes = nil, &block) values_for_primary_interface = {} - build_values_for_primary_interface!(values_for_primary_interface, args) + attributes = attributes&.with_indifferent_access + build_values_for_primary_interface!(values_for_primary_interface, attributes) - super(*args) + super(attributes, &block) build_required_interfaces update_primary_interface_attributes(values_for_primary_interface) @@ -401,19 +402,17 @@ def parse_ip_address(address, ignore_link_local: true) addr.to_s end - def build_values_for_primary_interface!(values_for_primary_interface, args) - new_attrs = args.shift - unless new_attrs.nil? - new_attrs = new_attrs.with_indifferent_access - values_for_primary_interface[:name] = NameGenerator.new.next_random_name unless new_attrs.has_key?(:name) + def build_values_for_primary_interface!(values_for_primary_interface, attributes) + unless attributes.nil? + values_for_primary_interface[:name] = NameGenerator.new.next_random_name unless attributes.has_key?(:name) PRIMARY_INTERFACE_ATTRIBUTES.each do |attr| - values_for_primary_interface[attr] = new_attrs.delete(attr) if new_attrs.has_key?(attr) + values_for_primary_interface[attr] = attributes.delete(attr) if attributes.has_key?(attr) end - model_name = new_attrs.delete(:model_name) - new_attrs[:hardware_model_name] = model_name if model_name.present? + model_name = attributes.delete(:model_name) + attributes[:hardware_model_name] = model_name if model_name.present? - args.unshift(new_attrs) + attributes end end diff --git a/app/models/host/managed.rb b/app/models/host/managed.rb index b9afcb4ade8b..e63032e5a5b2 100644 --- a/app/models/host/managed.rb +++ b/app/models/host/managed.rb @@ -73,9 +73,8 @@ def self.complete_for(query, opts = {}) include PxeLoaderValidator - def initialize(*args) - args.unshift(apply_inherited_attributes(args.shift, false)) - super(*args) + def initialize(attributes = nil, &block) + super(apply_inherited_attributes(attributes, false), &block) end def build_hooks diff --git a/app/models/subnet.rb b/app/models/subnet.rb index a82dbccf69ab..6922ea986c2f 100644 --- a/app/models/subnet.rb +++ b/app/models/subnet.rb @@ -422,9 +422,9 @@ def subnet_for(ip) end # This casts Subnet to Subnet::Ipv4 if no type is set - def new(*attributes, &block) - type = attributes.first.with_indifferent_access.delete(:type) if attributes.first.is_a?(Hash) - return Subnet::Ipv4.new(*attributes, &block) if self == Subnet && type.nil? + def new(attributes = nil, &block) + type = attributes.with_indifferent_access.delete(:type) if attributes.is_a?(Hash) + return Subnet::Ipv4.new(attributes, &block) if self == Subnet && type.nil? super end diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb index c209c2171b34..bdda9aab17f8 100644 --- a/test/helpers/application_helper_test.rb +++ b/test/helpers/application_helper_test.rb @@ -33,13 +33,13 @@ def test_generate_link_for end test '#documentation_url and new docs page' do - url = documentation_url('TestSection', { type: 'plugin_manual', name: 'foreman_my_plugin', version: '1.2' }) + url = documentation_url('TestSection', type: 'plugin_manual', name: 'foreman_my_plugin', version: '1.2') assert_match %r{links/plugin_manual/TestSection\?name=foreman_my_plugin&version=1\.2}, url end test '#documentation_url and new docs page' do - url = documentation_url('TestSection', { type: 'docs', chapter: 'test_chapter' }) + url = documentation_url('TestSection', type: 'docs', chapter: 'test_chapter') assert_match %r{links/docs/TestSection\?chapter=test_chapter}, url end