diff --git a/lib/puppet/pops/types/type_parser.rb b/lib/puppet/pops/types/type_parser.rb index 1b8cc8948..782598f2d 100644 --- a/lib/puppet/pops/types/type_parser.rb +++ b/lib/puppet/pops/types/type_parser.rb @@ -1,460 +1,469 @@ # This class provides parsing of Type Specification from a string into the Type # Model that is produced by the Puppet::Pops::Types::TypeFactory. # # The Type Specifications that are parsed are the same as the stringified forms # of types produced by the {Puppet::Pops::Types::TypeCalculator TypeCalculator}. # # @api public class Puppet::Pops::Types::TypeParser # @api private TYPES = Puppet::Pops::Types::TypeFactory # @api public def initialize @parser = Puppet::Pops::Parser::Parser.new() @type_transformer = Puppet::Pops::Visitor.new(nil, "interpret", 0, 0) end # Produces a *puppet type* based on the given string. # # @example # parser.parse('Integer') # parser.parse('Array[String]') # parser.parse('Hash[Integer, Array[String]]') # # @param string [String] a string with the type expressed in stringified form as produced by the # {Puppet::Pops::Types::TypeCalculator#string TypeCalculator#string} method. # @return [Puppet::Pops::Types::PObjectType] a specialization of the PObjectType representing the type. # # @api public # def parse(string) # TODO: This state (@string) can be removed since the parse result of newer future parser # contains a Locator in its SourcePosAdapter and the Locator keeps the string. # This way, there is no difference between a parsed "string" and something that has been parsed # earlier and fed to 'interpret' # @string = string model = @parser.parse_string(@string) if model interpret(model.current) else raise_invalid_type_specification_error end end # @api private def interpret(ast) result = @type_transformer.visit_this_0(self, ast) result = result.body if result.is_a?(Puppet::Pops::Model::Program) raise_invalid_type_specification_error unless result.is_a?(Puppet::Pops::Types::PAbstractType) result end # @api private def interpret_any(ast) @type_transformer.visit_this_0(self, ast) end # @api private def interpret_Object(o) raise_invalid_type_specification_error end # @api private def interpret_Program(o) interpret(o.body) end # @api private def interpret_QualifiedName(o) o.value end # @api private def interpret_LiteralString(o) o.value end # @api private def interpret_String(o) o end # @api private def interpret_LiteralDefault(o) :default end # @api private def interpret_LiteralInteger(o) o.value end # @api private def interpret_LiteralFloat(o) o.value end # @api private def interpret_LiteralHash(o) result = {} o.entries.each do |entry| result[@type_transformer.visit_this_0(self, entry.key)] = @type_transformer.visit_this_0(self, entry.value) end result end # @api private def interpret_QualifiedReference(name_ast) case name_ast.value when "integer" TYPES.integer when "float" TYPES.float when "numeric" TYPES.numeric when "string" TYPES.string when "enum" TYPES.enum when "boolean" TYPES.boolean when "pattern" TYPES.pattern when "regexp" TYPES.regexp when "data" TYPES.data when "array" TYPES.array_of_data when "hash" TYPES.hash_of_data when "class" TYPES.host_class() when "resource" TYPES.resource() when "collection" TYPES.collection() when "scalar" TYPES.scalar() when "catalogentry" TYPES.catalog_entry() when "undef" # Should not be interpreted as Resource type TYPES.undef() when "object" TYPES.object() when "variant" TYPES.variant() when "optional" TYPES.optional() when "ruby" TYPES.ruby_type() when "type" TYPES.type_type() when "tuple" TYPES.tuple() when "struct" TYPES.struct() + + when "callable" + # A generic callable as opposed to one that does not accept arguments + TYPES.all_callables() + else TYPES.resource(name_ast.value) end end # @api private def interpret_AccessExpression(parameterized_ast) parameters = parameterized_ast.keys.collect { |param| interpret_any(param) } unless parameterized_ast.left_expr.is_a?(Puppet::Pops::Model::QualifiedReference) raise_invalid_type_specification_error end case parameterized_ast.left_expr.value when "array" case parameters.size when 1 when 2 size_type = if parameters[1].is_a?(Puppet::Pops::Types::PIntegerType) parameters[1].copy else assert_range_parameter(parameters[1]) TYPES.range(parameters[1], :default) end when 3 assert_range_parameter(parameters[1]) assert_range_parameter(parameters[2]) size_type = TYPES.range(parameters[1], parameters[2]) else raise_invalid_parameters_error("Array", "1 to 3", parameters.size) end assert_type(parameters[0]) t = TYPES.array_of(parameters[0]) t.size_type = size_type if size_type t when "hash" result = case parameters.size when 1 assert_type(parameters[0]) TYPES.hash_of(parameters[0]) when 2 assert_type(parameters[0]) assert_type(parameters[1]) TYPES.hash_of(parameters[1], parameters[0]) when 3 size_type = if parameters[2].is_a?(Puppet::Pops::Types::PIntegerType) parameters[2].copy else assert_range_parameter(parameters[2]) TYPES.range(parameters[2], :default) end assert_type(parameters[0]) assert_type(parameters[1]) TYPES.hash_of(parameters[1], parameters[0]) when 4 assert_range_parameter(parameters[2]) assert_range_parameter(parameters[3]) size_type = TYPES.range(parameters[2], parameters[3]) assert_type(parameters[0]) assert_type(parameters[1]) TYPES.hash_of(parameters[1], parameters[0]) else raise_invalid_parameters_error("Hash", "1 to 4", parameters.size) end result.size_type = size_type if size_type result when "collection" size_type = case parameters.size when 1 if parameters[0].is_a?(Puppet::Pops::Types::PIntegerType) parameters[0].copy else assert_range_parameter(parameters[0]) TYPES.range(parameters[0], :default) end when 2 assert_range_parameter(parameters[0]) assert_range_parameter(parameters[1]) TYPES.range(parameters[0], parameters[1]) else raise_invalid_parameters_error("Collection", "1 to 2", parameters.size) end result = TYPES.collection result.size_type = size_type result when "class" if parameters.size != 1 raise_invalid_parameters_error("Class", 1, parameters.size) end TYPES.host_class(parameters[0]) when "resource" if parameters.size == 1 TYPES.resource(parameters[0]) elsif parameters.size != 2 raise_invalid_parameters_error("Resource", "1 or 2", parameters.size) else TYPES.resource(parameters[0], parameters[1]) end when "regexp" # 1 parameter being a string, or regular expression raise_invalid_parameters_error("Regexp", "1", parameters.size) unless parameters.size == 1 TYPES.regexp(parameters[0]) when "enum" # 1..m parameters being strings raise_invalid_parameters_error("Enum", "1 or more", parameters.size) unless parameters.size > 1 TYPES.enum(*parameters) when "pattern" # 1..m parameters being strings or regular expressions raise_invalid_parameters_error("Pattern", "1 or more", parameters.size) unless parameters.size > 1 TYPES.pattern(*parameters) when "variant" # 1..m parameters being strings or regular expressions raise_invalid_parameters_error("Variant", "1 or more", parameters.size) unless parameters.size > 1 TYPES.variant(*parameters) when "tuple" # 1..m parameters being types (last two optionally integer or literal default raise_invalid_parameters_error("Tuple", "1 or more", parameters.size) unless parameters.size > 1 length = parameters.size if TYPES.is_range_parameter?(parameters[-2]) # min, max specification min = parameters[-2] min = (min == :default || min == 'default') ? 0 : min assert_range_parameter(parameters[-1]) max = parameters[-1] max = max == :default ? nil : max parameters = parameters[0, length-2] elsif TYPES.is_range_parameter?(parameters[-1]) min = parameters[-1] min = (min == :default || min == 'default') ? 0 : min max = nil parameters = parameters[0, length-1] end t = TYPES.tuple(*parameters) if min || max TYPES.constrain_size(t, min, max) end t + when "callable" + # 1..m parameters being types (last three optionally integer or literal default, and a callable) + TYPES.callable(*parameters) + when "struct" # 1..m parameters being types (last two optionally integer or literal default raise_invalid_parameters_error("Struct", "1", parameters.size) unless parameters.size == 1 assert_struct_parameter(parameters[0]) TYPES.struct(parameters[0]) when "integer" if parameters.size == 1 case parameters[0] when Integer TYPES.range(parameters[0], parameters[0]) when :default TYPES.integer # unbound end elsif parameters.size != 2 raise_invalid_parameters_error("Integer", "1 or 2", parameters.size) else TYPES.range(parameters[0] == :default ? nil : parameters[0], parameters[1] == :default ? nil : parameters[1]) end when "float" if parameters.size == 1 case parameters[0] when Integer, Float TYPES.float_range(parameters[0], parameters[0]) when :default TYPES.float # unbound end elsif parameters.size != 2 raise_invalid_parameters_error("Float", "1 or 2", parameters.size) else TYPES.float_range(parameters[0] == :default ? nil : parameters[0], parameters[1] == :default ? nil : parameters[1]) end when "string" size_type = case parameters.size when 1 if parameters[0].is_a?(Puppet::Pops::Types::PIntegerType) parameters[0].copy else assert_range_parameter(parameters[0]) TYPES.range(parameters[0], :default) end when 2 assert_range_parameter(parameters[0]) assert_range_parameter(parameters[1]) TYPES.range(parameters[0], parameters[1]) else raise_invalid_parameters_error("String", "1 to 2", parameters.size) end result = TYPES.string result.size_type = size_type result when "optional" if parameters.size != 1 raise_invalid_parameters_error("Optional", 1, parameters.size) end assert_type(parameters[0]) TYPES.optional(parameters[0]) when "object", "data", "catalogentry", "boolean", "scalar", "undef", "numeric" raise_unparameterized_type_error(parameterized_ast.left_expr) when "type" if parameters.size != 1 raise_invalid_parameters_error("Type", 1, parameters.size) end assert_type(parameters[0]) TYPES.type_type(parameters[0]) when "ruby" raise_invalid_parameters_error("Ruby", "1", parameters.size) unless parameters.size == 1 TYPES.ruby_type(parameters[0]) else # It is a resource such a File['/tmp/foo'] type_name = parameterized_ast.left_expr.value if parameters.size != 1 raise_invalid_parameters_error(type_name.capitalize, 1, parameters.size) end TYPES.resource(type_name, parameters[0]) end end private def assert_type(t) raise_invalid_type_specification_error unless t.is_a?(Puppet::Pops::Types::PObjectType) true end def assert_range_parameter(t) raise_invalid_type_specification_error unless TYPES.is_range_parameter?(t) end def assert_struct_parameter(h) raise_invalid_type_specification_error unless h.is_a?(Hash) h.each do |k,v| # TODO: Should have stricter name rule raise_invalid_type_specification_error unless k.is_a?(String) && !k.empty? assert_type(v) end end def raise_invalid_type_specification_error raise Puppet::ParseError, "The expression <#{@string}> is not a valid type specification." end def raise_invalid_parameters_error(type, required, given) raise Puppet::ParseError, "Invalid number of type parameters specified: #{type} requires #{required}, #{given} provided" end def raise_unparameterized_type_error(ast) raise Puppet::ParseError, "Not a parameterized type <#{original_text_of(ast)}>" end def raise_unknown_type_error(ast) raise Puppet::ParseError, "Unknown type <#{original_text_of(ast)}>" end def original_text_of(ast) position = Puppet::Pops::Adapters::SourcePosAdapter.adapt(ast) position.extract_text() end end diff --git a/spec/unit/pops/types/type_parser_spec.rb b/spec/unit/pops/types/type_parser_spec.rb index df9194161..95595e55d 100644 --- a/spec/unit/pops/types/type_parser_spec.rb +++ b/spec/unit/pops/types/type_parser_spec.rb @@ -1,197 +1,219 @@ require 'spec_helper' require 'puppet/pops' describe Puppet::Pops::Types::TypeParser do extend RSpec::Matchers::DSL let(:parser) { Puppet::Pops::Types::TypeParser.new } let(:types) { Puppet::Pops::Types::TypeFactory } it "rejects a puppet expression" do expect { parser.parse("1 + 1") }.to raise_error(Puppet::ParseError, /The expression <1 \+ 1> is not a valid type specification/) end it "rejects a empty type specification" do expect { parser.parse("") }.to raise_error(Puppet::ParseError, /The expression <> is not a valid type specification/) end it "rejects an invalid type simple type" do expect { parser.parse("notAType") }.to raise_error(Puppet::ParseError, /The expression is not a valid type specification/) end it "rejects an unknown parameterized type" do expect { parser.parse("notAType[Integer]") }.to raise_error(Puppet::ParseError, /The expression is not a valid type specification/) end it "rejects an unknown type parameter" do expect { parser.parse("Array[notAType]") }.to raise_error(Puppet::ParseError, /The expression is not a valid type specification/) end [ 'Object', 'Data', 'CatalogEntry', 'Boolean', 'Scalar', 'Undef', 'Numeric', ].each do |name| it "does not support parameterizing unparameterized type <#{name}>" do expect { parser.parse("#{name}[Integer]") }.to raise_unparameterized_error_for(name) end end it "parses a simple, unparameterized type into the type object" do expect(the_type_parsed_from(types.object)).to be_the_type(types.object) expect(the_type_parsed_from(types.integer)).to be_the_type(types.integer) expect(the_type_parsed_from(types.float)).to be_the_type(types.float) expect(the_type_parsed_from(types.string)).to be_the_type(types.string) expect(the_type_parsed_from(types.boolean)).to be_the_type(types.boolean) expect(the_type_parsed_from(types.pattern)).to be_the_type(types.pattern) expect(the_type_parsed_from(types.data)).to be_the_type(types.data) expect(the_type_parsed_from(types.catalog_entry)).to be_the_type(types.catalog_entry) expect(the_type_parsed_from(types.collection)).to be_the_type(types.collection) expect(the_type_parsed_from(types.tuple)).to be_the_type(types.tuple) expect(the_type_parsed_from(types.struct)).to be_the_type(types.struct) expect(the_type_parsed_from(types.optional)).to be_the_type(types.optional) end it "interprets an unparameterized Array as an Array of Data" do expect(parser.parse("Array")).to be_the_type(types.array_of_data) end it "interprets an unparameterized Hash as a Hash of Scalar to Data" do expect(parser.parse("Hash")).to be_the_type(types.hash_of_data) end it "interprets a parameterized Hash[t] as a Hash of Scalar to t" do expect(parser.parse("Hash[Integer]")).to be_the_type(types.hash_of(types.integer)) end it "parses a parameterized type into the type object" do parameterized_array = types.array_of(types.integer) parameterized_hash = types.hash_of(types.integer, types.boolean) expect(the_type_parsed_from(parameterized_array)).to be_the_type(parameterized_array) expect(the_type_parsed_from(parameterized_hash)).to be_the_type(parameterized_hash) end it "parses a size constrained collection using capped range" do parameterized_array = types.array_of(types.integer) types.constrain_size(parameterized_array, 1,2) parameterized_hash = types.hash_of(types.integer, types.boolean) types.constrain_size(parameterized_hash, 1,2) expect(the_type_parsed_from(parameterized_array)).to be_the_type(parameterized_array) expect(the_type_parsed_from(parameterized_hash)).to be_the_type(parameterized_hash) end it "parses a size constrained collection with open range" do parameterized_array = types.array_of(types.integer) types.constrain_size(parameterized_array, 1,:default) parameterized_hash = types.hash_of(types.integer, types.boolean) types.constrain_size(parameterized_hash, 1,:default) expect(the_type_parsed_from(parameterized_array)).to be_the_type(parameterized_array) expect(the_type_parsed_from(parameterized_hash)).to be_the_type(parameterized_hash) end it "parses optional type" do opt_t = types.optional(Integer) expect(the_type_parsed_from(opt_t)).to be_the_type(opt_t) end it "parses tuple type" do tuple_t = types.tuple(Integer, String) expect(the_type_parsed_from(tuple_t)).to be_the_type(tuple_t) end it "parses tuple type with occurence constraint" do tuple_t = types.tuple(Integer, String) types.constrain_size(tuple_t, 2, 5) expect(the_type_parsed_from(tuple_t)).to be_the_type(tuple_t) end it "parses struct type" do struct_t = types.struct({'a'=>Integer, 'b'=>String}) expect(the_type_parsed_from(struct_t)).to be_the_type(struct_t) end it "rejects an collection spec with the wrong number of parameters" do expect { parser.parse("Array[Integer, 1,2,3]") }.to raise_the_parameter_error("Array", "1 to 3", 4) expect { parser.parse("Hash[Integer, Integer, 1,2,3]") }.to raise_the_parameter_error("Hash", "1 to 4", 5) end it "interprets anything that is not a built in type to be a resource type" do expect(parser.parse("File")).to be_the_type(types.resource('file')) end it "parses a resource type with title" do expect(parser.parse("File['/tmp/foo']")).to be_the_type(types.resource('file', '/tmp/foo')) end it "parses a resource type using 'Resource[type]' form" do expect(parser.parse("Resource[File]")).to be_the_type(types.resource('file')) end it "parses a resource type with title using 'Resource[type, title]'" do expect(parser.parse("Resource[File, '/tmp/foo']")).to be_the_type(types.resource('file', '/tmp/foo')) end it "parses a host class type" do expect(parser.parse("Class")).to be_the_type(types.host_class()) end it "parses a parameterized host class type" do expect(parser.parse("Class[foo::bar]")).to be_the_type(types.host_class('foo::bar')) end it 'parses an integer range' do expect(parser.parse("Integer[1,2]")).to be_the_type(types.range(1,2)) end it 'parses a float range' do expect(parser.parse("Float[1.0,2.0]")).to be_the_type(types.float_range(1.0,2.0)) end it 'parses a collection size range' do expect(parser.parse("Collection[1,2]")).to be_the_type(types.constrain_size(types.collection,1,2)) end it 'parses a type type' do expect(parser.parse("Type[Integer]")).to be_the_type(types.type_type(types.integer)) end it 'parses a ruby type' do expect(parser.parse("Ruby['Integer']")).to be_the_type(types.ruby_type('Integer')) end + it 'parses a callable type' do + expect(parser.parse("Callable")).to be_the_type(types.all_callables()) + end + + it 'parses a parameterized callable type' do + expect(parser.parse("Callable[String, Integer]")).to be_the_type(types.callable(String, Integer)) + end + + it 'parses a parameterized callable type with min/max' do + expect(parser.parse("Callable[String, Integer, 1, default]")).to be_the_type(types.callable(String, Integer, 1, :default)) + end + + it 'parses a parameterized callable type with block' do + expect(parser.parse("Callable[String, Callable[Boolean]]")).to be_the_type(types.callable(String, types.callable(true))) + end + + it 'parses a parameterized callable type with only min/max' do + t = parser.parse("Callable[0,0]") + expect(t).to be_the_type(types.callable()) + expect(t.param_types.types).to be_empty + end + matcher :be_the_type do |type| calc = Puppet::Pops::Types::TypeCalculator.new match do |actual| calc.assignable?(actual, type) && calc.assignable?(type, actual) end failure_message_for_should do |actual| "expected #{calc.string(type)}, but was #{calc.string(actual)}" end end def raise_the_parameter_error(type, required, given) raise_error(Puppet::ParseError, /#{type} requires #{required}, #{given} provided/) end def raise_type_error_for(type_name) raise_error(Puppet::ParseError, /Unknown type <#{type_name}>/) end def raise_unparameterized_error_for(type_name) raise_error(Puppet::ParseError, /Not a parameterized type <#{type_name}>/) end def the_type_parsed_from(type) parser.parse(the_type_spec_for(type)) end def the_type_spec_for(type) calc = Puppet::Pops::Types::TypeCalculator.new calc.string(type) end end