From 22c9909dc5b6da5f36bc4380917fb148ae13c4a2 Mon Sep 17 00:00:00 2001 From: Bernard Pietraga Date: Wed, 7 Nov 2018 14:58:31 +0100 Subject: [PATCH] Extract munging and validation logic to DataTypeHandling module The logic is extracted to lib/puppet/resource_api/data_type_handling.rb from lib/puppet/resource_api.rb This includes `mungify`, `mungify_core`, `try_mungify`, `validate`, `try_validate`, `parse_puppet_type`, `validate_ensure`, `ambiguous_error_msg`, `boolean_munge` methods. unpack_strings parameter is added to mungify which allows migration from legacy types. The expose API is backwards compatibile. --- lib/puppet/resource_api.rb | 173 +------- lib/puppet/resource_api/data_type_handling.rb | 196 +++++++++ lib/puppet/resource_api/type_definition.rb | 11 +- .../resource_api/data_type_handling_spec.rb | 410 ++++++++++++++++++ spec/puppet/resource_api_spec.rb | 335 -------------- 5 files changed, 639 insertions(+), 486 deletions(-) create mode 100644 lib/puppet/resource_api/data_type_handling.rb create mode 100644 spec/puppet/resource_api/data_type_handling_spec.rb diff --git a/lib/puppet/resource_api.rb b/lib/puppet/resource_api.rb index f593925a..3f692b94 100644 --- a/lib/puppet/resource_api.rb +++ b/lib/puppet/resource_api.rb @@ -1,4 +1,5 @@ require 'pathname' +require 'puppet/resource_api/data_type_handling' require 'puppet/resource_api/glue' require 'puppet/resource_api/puppet_context' unless RUBY_PLATFORM == 'java' require 'puppet/resource_api/type_definition' @@ -25,7 +26,7 @@ def register_type(definition) raise Puppet::DevError, '`:title_patterns` must be an array, not `%{other_type}`' % { other_type: definition[:title_patterns].class } end - validate_ensure(definition) + Puppet::ResourceApi::DataTypeHandling.validate_ensure(definition) definition[:features] ||= [] supported_features = %w[supports_noop canonicalize remote_resource simple_get_filter].freeze @@ -137,7 +138,10 @@ def to_resource return if @ral_find_absent definition[:attributes].each do |name, options| - type = Puppet::ResourceApi.parse_puppet_type(:name, options[:type]) + type = Puppet::ResourceApi::DataTypeHandling.parse_puppet_type( + :name, + options[:type], + ) # skip read only vars and the namevar next if [:read_only, :namevar].include? options[:behaviour] @@ -217,7 +221,10 @@ def insync?(is) end end - type = Puppet::ResourceApi.parse_puppet_type(name, options[:type]) + type = Puppet::ResourceApi::DataTypeHandling.parse_puppet_type( + name, + options[:type], + ) if param_or_property == :newproperty define_method(:should) do @@ -244,7 +251,14 @@ def insync?(is) # Puppet requires the @should value to always be stored as an array. We do not use this # for anything else # @see Puppet::Property.should=(value) - @should = [Puppet::ResourceApi.mungify(type, value, "#{definition[:name]}.#{name}")] + @should = [ + Puppet::ResourceApi::DataTypeHandling.mungify( + type, + value, + "#{definition[:name]}.#{name}", + Puppet::ResourceApi.caller_is_resource_app?, + ), + ] end # used internally @@ -262,7 +276,12 @@ def insync?(is) raise Puppet::ResourceError, "Attempting to set `#{name}` read_only attribute value to `#{value}`" end - @value = Puppet::ResourceApi.mungify(type, value, "#{definition[:name]}.#{name}") + @value = Puppet::ResourceApi::DataTypeHandling.mungify( + type, + value, + "#{definition[:name]}.#{name}", + Puppet::ResourceApi.caller_is_resource_app?, + ) end # used internally @@ -577,148 +596,4 @@ def self.def_newvalues(this, param_or_property, *values) def self.caller_is_resource_app? caller.any? { |c| c.match(%r{application/resource.rb:}) } end - - # This method handles translating values from the runtime environment to the expected types for the provider. - # When being called from `puppet resource`, it tries to transform the strings from the command line into their - # expected ruby representations, e.g. `"2"` (a string), will be transformed to `2` (the number) if (and only if) - # the target `type` is `Integer`. - # Additionally this function also validates that the passed in (and optionally transformed) value matches the - # specified type. - # @param type[Puppet::Pops::Types::TypedModelObject] the type to check/clean against - # @param value the value to clean - # @param error_msg_prefix[String] a prefix for the error messages - # @return [type] the cleaned value - # @raise [Puppet::ResourceError] if `value` could not be parsed into `type` - def self.mungify(type, value, error_msg_prefix) - if caller_is_resource_app? - # When the provider is exercised from the `puppet resource` CLI, we need to unpack strings into - # the correct types, e.g. "1" (a string) to 1 (an integer) - cleaned_value, error_msg = try_mungify(type, value, error_msg_prefix) - raise Puppet::ResourceError, error_msg if error_msg - elsif value == :false # rubocop:disable Lint/BooleanSymbol - # work around https://tickets.puppetlabs.com/browse/PUP-2368 - cleaned_value = false - elsif value == :true # rubocop:disable Lint/BooleanSymbol - # work around https://tickets.puppetlabs.com/browse/PUP-2368 - cleaned_value = true - else - # Every other time, we can use the values as is - cleaned_value = value - end - Puppet::ResourceApi.validate(type, cleaned_value, error_msg_prefix) - cleaned_value - end - - # Recursive implementation part of #mungify. Uses a multi-valued return value to avoid excessive - # exception throwing for regular usage - # @return [Array] if the mungify worked, the first element is the cleaned value, and the second - # element is nil. If the mungify failed, the first element is nil, and the second element is an error - # message - # @private - def self.try_mungify(type, value, error_msg_prefix) - case type - when Puppet::Pops::Types::PArrayType - if value.is_a? Array - conversions = value.map do |v| - try_mungify(type.element_type, v, error_msg_prefix) - end - # only convert the values if none failed. otherwise fall through and rely on puppet to render a proper error - if conversions.all? { |c| c[1].nil? } - value = conversions.map { |c| c[0] } - end - end - when Puppet::Pops::Types::PBooleanType - value = case value - when 'true', :true # rubocop:disable Lint/BooleanSymbol - true - when 'false', :false # rubocop:disable Lint/BooleanSymbol - false - else - value - end - when Puppet::Pops::Types::PIntegerType, Puppet::Pops::Types::PFloatType, Puppet::Pops::Types::PNumericType - if value =~ %r{^-?\d+$} || value =~ Puppet::Pops::Patterns::NUMERIC - value = Puppet::Pops::Utils.to_n(value) - end - when Puppet::Pops::Types::PEnumType, Puppet::Pops::Types::PStringType, Puppet::Pops::Types::PPatternType - if value.is_a? Symbol - value = value.to_s - end - when Puppet::Pops::Types::POptionalType - return value.nil? ? [nil, nil] : try_mungify(type.type, value, error_msg_prefix) - when Puppet::Pops::Types::PVariantType - # try converting to anything except string first - string_type = type.types.find { |t| t.is_a? Puppet::Pops::Types::PStringType } - conversion_results = (type.types - [string_type]).map do |t| - try_mungify(t, value, error_msg_prefix) - end - - # only consider valid results - conversion_results = conversion_results.select { |r| r[1].nil? }.to_a - - # use the conversion result if unambiguous - return conversion_results[0] if conversion_results.length == 1 - - # return an error if ambiguous - return [nil, "#{error_msg_prefix} #{value.inspect} is not unabiguously convertable to #{type}"] if conversion_results.length > 1 - - # try to interpret as string - return try_mungify(string_type, value, error_msg_prefix) if string_type - - # fall through to default handling - end - - error_msg = try_validate(type, value, error_msg_prefix) - if error_msg - # an error :-( - [nil, error_msg] - else - # a match! - [value, nil] - end - end - - # Validates the `value` against the specified `type`. - # @param type[Puppet::Pops::Types::TypedModelObject] the type to check against - # @param value the value to clean - # @param error_msg_prefix[String] a prefix for the error messages - # @raise [Puppet::ResourceError] if `value` is not of type `type` - # @private - def self.validate(type, value, error_msg_prefix) - error_msg = try_validate(type, value, error_msg_prefix) - - raise Puppet::ResourceError, error_msg if error_msg - end - - # Tries to validate the `value` against the specified `type`. - # @param type[Puppet::Pops::Types::TypedModelObject] the type to check against - # @param value the value to clean - # @param error_msg_prefix[String] a prefix for the error messages - # @return [String, nil] a error message indicating the problem, or `nil` if the value was valid. - # @private - def self.try_validate(type, value, error_msg_prefix) - return nil if type.instance?(value) - - # an error :-( - inferred_type = Puppet::Pops::Types::TypeCalculator.infer_set(value) - error_msg = Puppet::Pops::Types::TypeMismatchDescriber.new.describe_mismatch(error_msg_prefix, type, inferred_type) - error_msg - end - - def self.validate_ensure(definition) - return unless definition[:attributes].key? :ensure - options = definition[:attributes][:ensure] - type = Puppet::ResourceApi.parse_puppet_type(:ensure, options[:type]) - - return if type.is_a?(Puppet::Pops::Types::PEnumType) && type.values.sort == %w[absent present].sort - raise Puppet::DevError, '`:ensure` attribute must have a type of: `Enum[present, absent]`' - end - - def self.parse_puppet_type(attr_name, type) - Puppet::Pops::Types::TypeParser.singleton.parse(type) - rescue Puppet::ParseErrorWithIssue => e - raise Puppet::DevError, "The type of the `#{attr_name}` attribute `#{type}` could not be parsed: #{e.message}" - rescue Puppet::ParseError => e - raise Puppet::DevError, "The type of the `#{attr_name}` attribute `#{type}` is not recognised: #{e.message}" - end end diff --git a/lib/puppet/resource_api/data_type_handling.rb b/lib/puppet/resource_api/data_type_handling.rb new file mode 100644 index 00000000..909f1162 --- /dev/null +++ b/lib/puppet/resource_api/data_type_handling.rb @@ -0,0 +1,196 @@ +# This module is used to handle data inside types, contains methods for munging +# and validation of the type values. +module Puppet::ResourceApi::DataTypeHandling + # This method handles translating values from the runtime environment to the + # expected types for the provider with validation. + # When being called from `puppet resource`, it tries to transform the strings + # from the command line into their expected ruby representations, + # e.g. `"2"` (a string), will be transformed to `2` (the number) + # if (and only if) the target `type` is `Integer`. + # Additionally this function also validates that the passed in + # (and optionally transformed) value matches the specified type. + # @param type[Puppet::Pops::Types::TypedModelObject] the type to check/clean + # against. + # @param value the value to clean + # @param error_msg_prefix[String] a prefix for the error messages + # @param unpack_strings[Boolean] unpacking of strings for migrating off + # legacy type + # @return [type] the cleaned value + def self.mungify(type, value, error_msg_prefix, unpack_strings = false) + cleaned_value = mungify_core( + type, + value, + error_msg_prefix, + unpack_strings, + ) + validate(type, cleaned_value, error_msg_prefix) + cleaned_value + end + + # This is core method used in mungify which handles translating values to expected + # cleaned type values, result is not validated. + # @param type[Puppet::Pops::Types::TypedModelObject] the type to check/clean + # against. + # @param value the value to clean + # @param error_msg_prefix[String] a prefix for the error messages + # @param unpack_strings[Boolean] unpacking of strings for migrating off + # legacy type + # @return [type] the cleaned value + # @raise [Puppet::ResourceError] if `value` could not be parsed into `type` + # @private + def self.mungify_core(type, value, error_msg_prefix, unpack_strings = false) + if unpack_strings + # When the provider is exercised from the `puppet resource` CLI, we need + # to unpack strings into the correct types, e.g. "1" (a string) + # to 1 (an integer) + cleaned_value, error_msg = try_mungify(type, value, error_msg_prefix) + raise Puppet::ResourceError, error_msg if error_msg + cleaned_value + elsif value == :false # rubocop:disable Lint/BooleanSymbol + # work around https://tickets.puppetlabs.com/browse/PUP-2368 + false + elsif value == :true # rubocop:disable Lint/BooleanSymbol + # work around https://tickets.puppetlabs.com/browse/PUP-2368 + true + else + # Every other time, we can use the values as is + value + end + end + + # Recursive implementation part of #mungify_core. Uses a multi-valued return + # value to avoid excessive exception throwing for regular usage. + # @return [Array] if the mungify worked, the first element is the cleaned + # value, and the second element is nil. If the mungify failed, the first + # element is nil, and the second element is an error message + # @private + def self.try_mungify(type, value, error_msg_prefix) + case type + when Puppet::Pops::Types::PArrayType + if value.is_a? Array + conversions = value.map do |v| + try_mungify(type.element_type, v, error_msg_prefix) + end + # only convert the values if none failed. otherwise fall through and + # rely on puppet to render a proper error + if conversions.all? { |c| c[1].nil? } + value = conversions.map { |c| c[0] } + end + end + when Puppet::Pops::Types::PBooleanType + value = boolean_munge(value) + when Puppet::Pops::Types::PIntegerType, + Puppet::Pops::Types::PFloatType, + Puppet::Pops::Types::PNumericType + if value =~ %r{^-?\d+$} || value =~ Puppet::Pops::Patterns::NUMERIC + value = Puppet::Pops::Utils.to_n(value) + end + when Puppet::Pops::Types::PEnumType, + Puppet::Pops::Types::PStringType, + Puppet::Pops::Types::PPatternType + value = value.to_s if value.is_a? Symbol + when Puppet::Pops::Types::POptionalType + return value.nil? ? [nil, nil] : try_mungify(type.type, value, error_msg_prefix) + when Puppet::Pops::Types::PVariantType + # try converting to anything except string first + string_type = type.types.find { |t| t.is_a? Puppet::Pops::Types::PStringType } + conversion_results = (type.types - [string_type]).map do |t| + try_mungify(t, value, error_msg_prefix) + end + + # only consider valid results + conversion_results = conversion_results.select { |r| r[1].nil? }.to_a + + # use the conversion result if unambiguous + return conversion_results[0] if conversion_results.length == 1 + + # return an error if ambiguous + if conversion_results.length > 1 + return [nil, ambiguous_error_msg(error_msg_prefix, value, type)] + end + + # try to interpret as string + return try_mungify(string_type, value, error_msg_prefix) if string_type + + # fall through to default handling + end + + error_msg = try_validate(type, value, error_msg_prefix) + return [nil, error_msg] if error_msg # an error + [value, nil] # match + end + + # Returns correct boolean `value` based on one specified in `type`. + # @param value the value to boolean munge + # @private + def self.boolean_munge(value) + case value + when 'true', :true # rubocop:disable Lint/BooleanSymbol + true + when 'false', :false # rubocop:disable Lint/BooleanSymbol + false + else + value + end + end + + # Returns ambiguous error message based on `error_msg_prefix`, `value` and + # `type`. + # @param type[Puppet::Pops::Types::TypedModelObject] the type to check against + # @param value the value to clean + # @param error_msg_prefix[String] a prefix for the error messages + # @private + def self.ambiguous_error_msg(error_msg_prefix, value, type) + "#{error_msg_prefix} #{value.inspect} is not unabiguously convertable to " \ + "#{type}" + end + + # Validates the `value` against the specified `type`. + # @param type[Puppet::Pops::Types::TypedModelObject] the type to check against + # @param value the value to clean + # @param error_msg_prefix[String] a prefix for the error messages + # @raise [Puppet::ResourceError] if `value` is not of type `type` + # @private + def self.validate(type, value, error_msg_prefix) + error_msg = try_validate(type, value, error_msg_prefix) + raise Puppet::ResourceError, error_msg if error_msg + end + + # Tries to validate the `value` against the specified `type`. + # @param type[Puppet::Pops::Types::TypedModelObject] the type to check against + # @param value the value to clean + # @param error_msg_prefix[String] a prefix for the error messages + # @return [String, nil] a error message indicating the problem, or `nil` if the value was valid. + # @private + def self.try_validate(type, value, error_msg_prefix) + return nil if type.instance?(value) + + # an error :-( + inferred_type = Puppet::Pops::Types::TypeCalculator.infer_set(value) + error_msg = Puppet::Pops::Types::TypeMismatchDescriber.new.describe_mismatch( + error_msg_prefix, + type, + inferred_type, + ) + error_msg + end + + def self.validate_ensure(definition) + return unless definition[:attributes].key? :ensure + options = definition[:attributes][:ensure] + type = parse_puppet_type(:ensure, options[:type]) + + return if type.is_a?(Puppet::Pops::Types::PEnumType) && type.values.sort == %w[absent present].sort + raise Puppet::DevError, '`:ensure` attribute must have a type of: `Enum[present, absent]`' + end + + def self.parse_puppet_type(attr_name, type) + Puppet::Pops::Types::TypeParser.singleton.parse(type) + rescue Puppet::ParseErrorWithIssue => e + raise Puppet::DevError, "The type of the `#{attr_name}` attribute " \ + "`#{type}` could not be parsed: #{e.message}" + rescue Puppet::ParseError => e + raise Puppet::DevError, "The type of the `#{attr_name}` attribute " \ + "`#{type}` is not recognised: #{e.message}" + end +end diff --git a/lib/puppet/resource_api/type_definition.rb b/lib/puppet/resource_api/type_definition.rb index 4231bc44..029c2ac3 100644 --- a/lib/puppet/resource_api/type_definition.rb +++ b/lib/puppet/resource_api/type_definition.rb @@ -84,8 +84,15 @@ def check_schema_values(resource) bad_vals = {} resource.each do |key, value| next unless attributes[key] - type = Puppet::ResourceApi.parse_puppet_type(key, attributes[key][:type]) - error_message = Puppet::ResourceApi.try_validate(type, value, '') + type = Puppet::ResourceApi::DataTypeHandling.parse_puppet_type( + key, + attributes[key][:type], + ) + error_message = Puppet::ResourceApi::DataTypeHandling.try_validate( + type, + value, + '', + ) bad_vals[key] = value unless error_message.nil? end bad_vals diff --git a/spec/puppet/resource_api/data_type_handling_spec.rb b/spec/puppet/resource_api/data_type_handling_spec.rb new file mode 100644 index 00000000..bb9fd580 --- /dev/null +++ b/spec/puppet/resource_api/data_type_handling_spec.rb @@ -0,0 +1,410 @@ +require 'spec_helper' + +RSpec.describe Puppet::ResourceApi::DataTypeHandling do + let(:strict_level) { :error } + let(:log_sink) { [] } + + before(:each) do + # set default to strictest setting + # by default Puppet runs at warning level + Puppet.settings[:strict] = strict_level + # Enable debug logging + Puppet.debug = true + + Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(log_sink)) + end + + after(:each) do + Puppet::Util::Log.close_all + end + + describe '#mungify(type, value, unpack_strings = false)' do + context 'when called from `puppet resource`' do + before(:each) do + allow(described_class).to receive(:try_mungify).with('type', 'input', 'error prefix').and_return(result) + allow(described_class).to receive(:validate) + end + + let(:caller_is_resource_app) { true } + + context 'when the munge succeeds' do + let(:result) { ['result', nil] } + + it('returns the cleaned result') { expect(described_class.mungify('type', 'input', 'error prefix', caller_is_resource_app)).to eq 'result' } + it('validates the cleaned result') do + described_class.mungify('type', 'input', 'error prefix', caller_is_resource_app) + expect(described_class).to have_received(:validate).with('type', 'result', 'error prefix').once + end + end + + context 'when the munge fails' do + let(:result) { [nil, 'some error'] } + + it('raises the error') { expect { described_class.mungify('type', 'input', 'error prefix', caller_is_resource_app) }.to raise_error Puppet::ResourceError, %r{\Asome error\Z} } + end + end + + context 'when called from something else' do + before(:each) do + allow(described_class).to receive(:try_mungify).never + allow(described_class).to receive(:validate) + end + + let(:caller_is_resource_app) { false } + + it('returns the value') { expect(described_class.mungify('type', 'input', 'error prefix', caller_is_resource_app)).to eq 'input' } + it('validates the value') do + described_class.mungify('type', 'input', 'error prefix', caller_is_resource_app) + expect(described_class).to have_received(:validate).with('type', 'input', 'error prefix') + end + end + end + + # keep test data consistent # rubocop:disable Style/WordArray + # run try_mungify only once to get both value, and error # rubocop:disable RSpec/InstanceVariable + describe '#try_validate(type, value)' do + let(:error_msg) do + pops_type = Puppet::Pops::Types::TypeParser.singleton.parse(type) + described_class.try_validate(pops_type, value, 'error prefix') + end + + [ + { + type: 'String', + valid: ['a', 'true'], + invalid: [1, true], + }, + { + type: 'Integer', + valid: [1, -1, 0], + invalid: ['a', :a, 'true', 1.0], + }, + ].each do |testcase| + context "when validating '#{testcase[:type]}" do + let(:type) { testcase[:type] } + + testcase[:valid].each do |valid_value| + context "when validating #{valid_value.inspect}" do + let(:value) { valid_value } + + it { expect(error_msg).to be nil } + end + end + testcase[:invalid].each do |invalid_value| + context "when validating #{invalid_value.inspect}" do + let(:value) { invalid_value } + + it { expect(error_msg).to match %r{^error prefix } } + end + end + end + end + end + + describe '#try_mungify(type, value)' do + before(:each) do + @value, @error = described_class.try_mungify(type, input, 'error prefix') + end + + [ + { + type: 'Boolean', + transformations: [ + [true, true], + [:true, true], # rubocop:disable Lint/BooleanSymbol + ['true', true], + [false, false], + [:false, false], # rubocop:disable Lint/BooleanSymbol + ['false', false], + ], + errors: ['something', 'yes', 'no', 0, 1, -1, '1', 1.1, -1.1, '1.1', '-1.1', ''], + }, + { + type: 'Integer', + transformations: [ + [0, 0], + [1, 1], + ['1', 1], + [-1, -1], + ['-1', -1], + ], + errors: ['something', 1.1, -1.1, '1.1', '-1.1', '', :'1'], + }, + { + type: 'Float', + transformations: [ + [0.0, 0.0], + [1.5, 1.5], + ['1.5', 1.5], + [-1.5, -1.5], + ['-1.5', -1.5], + ], + errors: ['something', '', 0, '0', 1, '1', -1, '-1', :'1.1'], + }, + { + type: 'Numeric', + transformations: [ + [0, 0], + [1, 1], + ['1', 1], + [-1, -1], + ['-1', -1], + [0.0, 0.0], + [1.5, 1.5], + ['1.5', 1.5], + [-1.5, -1.5], + ['-1.5', -1.5], + ], + errors: ['something', '', true, :symbol, :'1'], + }, + { + type: 'String', + transformations: [ + ['', ''], + ['1', '1'], + [:'1', '1'], + ['-1', '-1'], + ['true', 'true'], + ['false', 'false'], + ['something', 'something'], + [:symbol, 'symbol'], + ], + errors: [1.1, -1.1, 1, -1, true, false], + }, + { + type: 'Enum[absent, present]', + transformations: [ + ['absent', 'absent'], + ['absent', 'absent'], + ['present', 'present'], + ['present', 'present'], + ], + errors: ['enabled', :something, 1, 'y', 'true', ''], + }, + { + type: 'Pattern[/\A(0x)?[0-9a-fA-F]{8}\Z/]', + transformations: [ + ['0xABCD1234', '0xABCD1234'], + ['ABCD1234', 'ABCD1234'], + [:'0xABCD1234', '0xABCD1234'], + ], + errors: [0xABCD1234, '1234567', 'enabled', 0, ''], + }, + { + type: 'Array', + transformations: [ + [[], []], + [[[]], [[]]], + [['a'], ['a']], + [['a', 1], ['a', 1]], + [['a', 'b', 'c'], ['a', 'b', 'c']], + [[true, 'a', 1], [true, 'a', 1]], + ], + errors: ['enabled', :something, 1, 'y', 'true', ''], + }, + { + type: 'Array[Boolean]', + transformations: [ + [[], []], + [[true], [true]], + [[:true], [true]], # rubocop:disable Lint/BooleanSymbol + [['true'], [true]], + [[false], [false]], + [[:false], [false]], # rubocop:disable Lint/BooleanSymbol + [['false'], [false]], + [[true, 'false'], [true, false]], + [[true, true, true, 'false'], [true, true, true, false]], + ], + errors: [['something'], ['yes'], ['no'], [0], true, false], + }, + { + type: 'Array[Integer]', + transformations: [ + [[], []], + [[1], [1]], + [['1'], [1]], + [['1', 2, '3'], [1, 2, 3]], + ], + errors: ['enabled', :something, 1, 'y', 'true', '', [true, 'a'], [1, 'b', 3], [[]]], + }, + { + type: 'Array[Float]', + transformations: [ + [[], []], + [[1.0], [1.0]], + [['1.0'], [1.0]], + [['1.0', 2.0, '3.0'], [1.0, 2.0, 3.0]], + ], + errors: ['enabled', :something, 1, 'y', 'true', '', [true, 'a'], [1.0, 'b', 3.0], [[]]], + }, + { + type: 'Array[Numeric]', + transformations: [ + [[], []], + [[1], [1]], + [['1'], [1]], + [[1.0], [1.0]], + [['1.0'], [1.0]], + [['1.0', 2, '3'], [1.0, 2, 3]], + [['1.0', 2.0, '3.0'], [1.0, 2.0, 3.0]], + ], + errors: ['enabled', :something, 1, 'y', 'true', '', [true, 'a'], [1.0, 'b', 3.0], [[]]], + }, + { + type: 'Array[String]', + transformations: [ + [[], []], + [['a'], ['a']], + [['a', 'b', 'c'], ['a', 'b', 'c']], + ], + errors: ['enabled', :something, 1, 'y', 'true', '', [1], ['a', 1, 'b'], [true, 'a'], [[]]], + }, + { + # When requesting a Variant type, expect values to be transformed according to the rules of the constituent types. + # Always try to up-convert, falling back to String only when necessary/possible. + # Conversions need to be unambiguous to be valid. This should only be ever hit in pathological cases like + # Variant[Integer, Float], or Variant[Boolean, Enum[true, false]] + type: 'Variant[Boolean, String, Integer]', + transformations: [ + [true, true], + [:true, true], # rubocop:disable Lint/BooleanSymbol + ['true', true], + [false, false], + [:false, false], # rubocop:disable Lint/BooleanSymbol + ['false', false], + [0, 0], + [1, 1], + ['1', 1], + [-1, -1], + ['-1', -1], + ['something', 'something'], + [:symbol, 'symbol'], + ['1.1', '1.1'], + ], + errors: [1.0, [1.0], ['1']], + }, + { + type: 'Variant[Integer, Enum[a, "2", "3"]]', + transformations: [ + [1, 1], + ['a', 'a'], + ], + errors: ['2', '3'], + }, + { + type: 'Variant[Array[Variant[Integer,String]],Boolean]', + transformations: [ + [true, true], + [:false, false], # rubocop:disable Lint/BooleanSymbol + [[1], [1]], + [['1'], [1]], + [['1', 'a'], [1, 'a']], + ], + errors: [ + [:something, [1.0]], + ], + }, + ].each do |type_test| + context "with a #{type_test[:type]} type" do + let(:type) { Puppet::Pops::Types::TypeParser.singleton.parse(type_test[:type]) } + + type_test[:transformations].each do |input, output| + context "with #{input.inspect} as value" do + let(:input) { input } + + it("transforms to #{output.inspect}") { expect(@value).to eq output } + it('returns no error') { expect(@error).to be_nil } + end + end + + ([nil] + type_test[:errors]).each do |input| + context "with #{input.inspect} as value" do + let(:input) { input } + + it('returns no value') { expect(@value).to be_nil } + it('returns an error') { expect(@error).to match %r{\A\s*error prefix} } + end + end + end + + context "with a Optional[#{type_test[:type]}] type" do + let(:type) { Puppet::Pops::Types::TypeParser.singleton.parse("Optional[#{type_test[:type]}]") } + + ([[nil, nil]] + type_test[:transformations]).each do |input, output| + context "with #{input.inspect} as value" do + let(:input) { input } + + it("transforms to #{output.inspect}") { expect(@value).to eq output } + it("returns no error for #{input.inspect}") { expect(@error).to be_nil } + end + end + + type_test[:errors].each do |input| + context "with #{input.inspect} as value" do + let(:input) { input } + + it('returns no value') { expect(@value).to be_nil } + it('returns an error') { expect(@error).to match %r{\A\s*error prefix} } + end + end + end + end + end + + describe '#ambiguous_error_msg(error_msg_prefix, value, type)' do + context 'with a Integer type' do + context 'with a string value' do + let(:type) { Puppet::Pops::Types::TypeParser.singleton.parse('Integer') } + let(:value) { 'a' } + let(:error_msg_prefix) { 'prefix' } + let(:result) { 'prefix "a" is not unabiguously convertable to Integer' } + + it('outputs error message') do + expect(described_class.ambiguous_error_msg(error_msg_prefix, value, type)).to eq result + end + end + end + end + + describe '#boolean_munge(value)' do + context 'when the munge succeeds' do + [ + { + value: 'true', + result: true, + }, + { + value: :true, # rubocop:disable Lint/BooleanSymbol + result: true, + }, + { + value: true, + result: true, + }, + { + value: 'false', + result: false, + }, + { + value: :false, # rubocop:disable Lint/BooleanSymbol + result: false, + }, + { + value: false, + result: false, + }, + ].each do |munge_test| + context "with a #{munge_test[:value].class} value" do + let(:input) { munge_test[:value] } + let(:result) { munge_test[:result] } + + it("transforms to #{munge_test[:result]}") do + expect(described_class.boolean_munge(input)).to eq result + end + end + end + end + end + # rubocop:enable Style/WordArray + # rubocop:enable RSpec/InstanceVariable +end diff --git a/spec/puppet/resource_api_spec.rb b/spec/puppet/resource_api_spec.rb index 75ae1e20..f383e72d 100644 --- a/spec/puppet/resource_api_spec.rb +++ b/spec/puppet/resource_api_spec.rb @@ -1902,339 +1902,4 @@ def set(_context, changes) end it { expect { described_class.register_type(definition) }.to raise_error Puppet::ResourceError, %r{^`bad` is not a valid behaviour value$} } end end - - describe '#mungify(type, value)' do - context 'when called from `puppet resource`' do - before(:each) do - allow(described_class).to receive(:caller_is_resource_app?).with(no_args).and_return(true) - allow(described_class).to receive(:try_mungify).with('type', 'input', 'error prefix').and_return(result) - allow(described_class).to receive(:validate) - end - - let(:caller_is_resource_app) { true } - - context 'when the munge succeeds' do - let(:result) { ['result', nil] } - - it('returns the cleaned result') { expect(described_class.mungify('type', 'input', 'error prefix')).to eq 'result' } - it('validates the cleaned result') do - described_class.mungify('type', 'input', 'error prefix') - expect(described_class).to have_received(:validate).with('type', 'result', 'error prefix').once - end - end - - context 'when the munge fails' do - let(:result) { [nil, 'some error'] } - - it('raises the error') { expect { described_class.mungify('type', 'input', 'error prefix') }.to raise_error Puppet::ResourceError, %r{\Asome error\Z} } - end - end - - context 'when called from something else' do - before(:each) do - allow(described_class).to receive(:caller_is_resource_app?).with(no_args).and_return(false) - allow(described_class).to receive(:try_mungify).never - allow(described_class).to receive(:validate) - end - - it('returns the value') { expect(described_class.mungify('type', 'input', 'error prefix')).to eq 'input' } - it('validates the value') do - described_class.mungify('type', 'input', 'error prefix') - expect(described_class).to have_received(:validate).with('type', 'input', 'error prefix') - end - end - end - - # keep test data consistent # rubocop:disable Style/WordArray - # run try_mungify only once to get both value, and error # rubocop:disable RSpec/InstanceVariable - describe '#try_validate(type, value)' do - let(:error_msg) do - pops_type = Puppet::Pops::Types::TypeParser.singleton.parse(type) - described_class.try_validate(pops_type, value, 'error prefix') - end - - [ - { - type: 'String', - valid: ['a', 'true'], - invalid: [1, true], - }, - { - type: 'Integer', - valid: [1, -1, 0], - invalid: ['a', :a, 'true', 1.0], - }, - ].each do |testcase| - context "when validating '#{testcase[:type]}" do - let(:type) { testcase[:type] } - - testcase[:valid].each do |valid_value| - context "when validating #{valid_value.inspect}" do - let(:value) { valid_value } - - it { expect(error_msg).to be nil } - end - end - testcase[:invalid].each do |invalid_value| - context "when validating #{invalid_value.inspect}" do - let(:value) { invalid_value } - - it { expect(error_msg).to match %r{^error prefix } } - end - end - end - end - end - - describe '#try_mungify(type, value)' do - before(:each) do - @value, @error = described_class.try_mungify(type, input, 'error prefix') - end - - [ - { - type: 'Boolean', - transformations: [ - [true, true], - [:true, true], # rubocop:disable Lint/BooleanSymbol - ['true', true], - [false, false], - [:false, false], # rubocop:disable Lint/BooleanSymbol - ['false', false], - ], - errors: ['something', 'yes', 'no', 0, 1, -1, '1', 1.1, -1.1, '1.1', '-1.1', ''], - }, - { - type: 'Integer', - transformations: [ - [0, 0], - [1, 1], - ['1', 1], - [-1, -1], - ['-1', -1], - ], - errors: ['something', 1.1, -1.1, '1.1', '-1.1', '', :'1'], - }, - { - type: 'Float', - transformations: [ - [0.0, 0.0], - [1.5, 1.5], - ['1.5', 1.5], - [-1.5, -1.5], - ['-1.5', -1.5], - ], - errors: ['something', '', 0, '0', 1, '1', -1, '-1', :'1.1'], - }, - { - type: 'Numeric', - transformations: [ - [0, 0], - [1, 1], - ['1', 1], - [-1, -1], - ['-1', -1], - [0.0, 0.0], - [1.5, 1.5], - ['1.5', 1.5], - [-1.5, -1.5], - ['-1.5', -1.5], - ], - errors: ['something', '', true, :symbol, :'1'], - }, - { - type: 'String', - transformations: [ - ['', ''], - ['1', '1'], - [:'1', '1'], - ['-1', '-1'], - ['true', 'true'], - ['false', 'false'], - ['something', 'something'], - [:symbol, 'symbol'], - ], - errors: [1.1, -1.1, 1, -1, true, false], - }, - { - type: 'Enum[absent, present]', - transformations: [ - ['absent', 'absent'], - ['absent', 'absent'], - ['present', 'present'], - ['present', 'present'], - ], - errors: ['enabled', :something, 1, 'y', 'true', ''], - }, - { - type: 'Pattern[/\A(0x)?[0-9a-fA-F]{8}\Z/]', - transformations: [ - ['0xABCD1234', '0xABCD1234'], - ['ABCD1234', 'ABCD1234'], - [:'0xABCD1234', '0xABCD1234'], - ], - errors: [0xABCD1234, '1234567', 'enabled', 0, ''], - }, - { - type: 'Array', - transformations: [ - [[], []], - [[[]], [[]]], - [['a'], ['a']], - [['a', 1], ['a', 1]], - [['a', 'b', 'c'], ['a', 'b', 'c']], - [[true, 'a', 1], [true, 'a', 1]], - ], - errors: ['enabled', :something, 1, 'y', 'true', ''], - }, - { - type: 'Array[Boolean]', - transformations: [ - [[], []], - [[true], [true]], - [[:true], [true]], # rubocop:disable Lint/BooleanSymbol - [['true'], [true]], - [[false], [false]], - [[:false], [false]], # rubocop:disable Lint/BooleanSymbol - [['false'], [false]], - [[true, 'false'], [true, false]], - [[true, true, true, 'false'], [true, true, true, false]], - ], - errors: [['something'], ['yes'], ['no'], [0], true, false], - }, - { - type: 'Array[Integer]', - transformations: [ - [[], []], - [[1], [1]], - [['1'], [1]], - [['1', 2, '3'], [1, 2, 3]], - ], - errors: ['enabled', :something, 1, 'y', 'true', '', [true, 'a'], [1, 'b', 3], [[]]], - }, - { - type: 'Array[Float]', - transformations: [ - [[], []], - [[1.0], [1.0]], - [['1.0'], [1.0]], - [['1.0', 2.0, '3.0'], [1.0, 2.0, 3.0]], - ], - errors: ['enabled', :something, 1, 'y', 'true', '', [true, 'a'], [1.0, 'b', 3.0], [[]]], - }, - { - type: 'Array[Numeric]', - transformations: [ - [[], []], - [[1], [1]], - [['1'], [1]], - [[1.0], [1.0]], - [['1.0'], [1.0]], - [['1.0', 2, '3'], [1.0, 2, 3]], - [['1.0', 2.0, '3.0'], [1.0, 2.0, 3.0]], - ], - errors: ['enabled', :something, 1, 'y', 'true', '', [true, 'a'], [1.0, 'b', 3.0], [[]]], - }, - { - type: 'Array[String]', - transformations: [ - [[], []], - [['a'], ['a']], - [['a', 'b', 'c'], ['a', 'b', 'c']], - ], - errors: ['enabled', :something, 1, 'y', 'true', '', [1], ['a', 1, 'b'], [true, 'a'], [[]]], - }, - { - # When requesting a Variant type, expect values to be transformed according to the rules of the constituent types. - # Always try to up-convert, falling back to String only when necessary/possible. - # Conversions need to be unambiguous to be valid. This should only be ever hit in pathological cases like - # Variant[Integer, Float], or Variant[Boolean, Enum[true, false]] - type: 'Variant[Boolean, String, Integer]', - transformations: [ - [true, true], - [:true, true], # rubocop:disable Lint/BooleanSymbol - ['true', true], - [false, false], - [:false, false], # rubocop:disable Lint/BooleanSymbol - ['false', false], - [0, 0], - [1, 1], - ['1', 1], - [-1, -1], - ['-1', -1], - ['something', 'something'], - [:symbol, 'symbol'], - ['1.1', '1.1'], - ], - errors: [1.0, [1.0], ['1']], - }, - { - type: 'Variant[Integer, Enum[a, "2", "3"]]', - transformations: [ - [1, 1], - ['a', 'a'], - ], - errors: ['2', '3'], - }, - { - type: 'Variant[Array[Variant[Integer,String]],Boolean]', - transformations: [ - [true, true], - [:false, false], # rubocop:disable Lint/BooleanSymbol - [[1], [1]], - [['1'], [1]], - [['1', 'a'], [1, 'a']], - ], - errors: [ - [:something, [1.0]], - ], - }, - ].each do |type_test| - context "with a #{type_test[:type]} type" do - let(:type) { Puppet::Pops::Types::TypeParser.singleton.parse(type_test[:type]) } - - type_test[:transformations].each do |input, output| - context "with #{input.inspect} as value" do - let(:input) { input } - - it("transforms to #{output.inspect}") { expect(@value).to eq output } - it('returns no error') { expect(@error).to be_nil } - end - end - - ([nil] + type_test[:errors]).each do |input| - context "with #{input.inspect} as value" do - let(:input) { input } - - it('returns no value') { expect(@value).to be_nil } - it('returns an error') { expect(@error).to match %r{\A\s*error prefix} } - end - end - end - - context "with a Optional[#{type_test[:type]}] type" do - let(:type) { Puppet::Pops::Types::TypeParser.singleton.parse("Optional[#{type_test[:type]}]") } - - ([[nil, nil]] + type_test[:transformations]).each do |input, output| - context "with #{input.inspect} as value" do - let(:input) { input } - - it("transforms to #{output.inspect}") { expect(@value).to eq output } - it("returns no error for #{input.inspect}") { expect(@error).to be_nil } - end - end - - type_test[:errors].each do |input| - context "with #{input.inspect} as value" do - let(:input) { input } - - it('returns no value') { expect(@value).to be_nil } - it('returns an error') { expect(@error).to match %r{\A\s*error prefix} } - end - end - end - end - end - # rubocop:enable Style/WordArray - # rubocop:enable RSpec/InstanceVariable end