Current File : //opt/puppetlabs/puppet/lib/ruby/vendor_ruby/puppet/module_tool/metadata.rb |
require_relative '../../puppet/module_tool'
require_relative '../../puppet/network/format_support'
require 'uri'
require_relative '../../puppet/util/json'
require 'set'
module Puppet::ModuleTool
# This class provides a data structure representing a module's metadata.
# @api private
class Metadata
include Puppet::Network::FormatSupport
attr_accessor :module_name
DEFAULTS = {
'name' => nil,
'version' => nil,
'author' => nil,
'summary' => nil,
'license' => 'Apache-2.0',
'source' => '',
'project_page' => nil,
'issues_url' => nil,
'dependencies' => Set.new.freeze,
'data_provider' => nil,
}
def initialize
@data = DEFAULTS.dup
@data['dependencies'] = @data['dependencies'].dup
end
# Returns a filesystem-friendly version of this module name.
def dashed_name
@data['name'].tr('/', '-') if @data['name']
end
# Returns a string that uniquely represents this version of this module.
def release_name
return nil unless @data['name'] && @data['version']
[ dashed_name, @data['version'] ].join('-')
end
alias :name :module_name
alias :full_module_name :dashed_name
# Merges the current set of metadata with another metadata hash. This
# method also handles the validation of module names and versions, in an
# effort to be proactive about module publishing constraints.
def update(data)
process_name(data) if data['name']
process_version(data) if data['version']
process_source(data) if data['source']
process_data_provider(data) if data['data_provider']
merge_dependencies(data) if data['dependencies']
@data.merge!(data)
return self
end
# Validates the name and version_requirement for a dependency, then creates
# the Dependency and adds it.
# Returns the Dependency that was added.
def add_dependency(name, version_requirement=nil, repository=nil)
validate_name(name)
validate_version_range(version_requirement) if version_requirement
dup = @data['dependencies'].find { |d| d.full_module_name == name && d.version_requirement != version_requirement }
raise ArgumentError, _("Dependency conflict for %{module_name}: Dependency %{name} was given conflicting version requirements %{version_requirement} and %{dup_version}. Verify that there are no duplicates in the metadata.json.") % { module_name: full_module_name, name: name, version_requirement: version_requirement, dup_version: dup.version_requirement } if dup
dep = Dependency.new(name, version_requirement, repository)
@data['dependencies'].add(dep)
dep
end
# Provides an accessor for the now defunct 'description' property. This
# addresses a regression in Puppet 3.6.x where previously valid templates
# referring to the 'description' property were broken.
# @deprecated
def description
@data['description']
end
def dependencies
@data['dependencies'].to_a
end
# Returns a hash of the module's metadata. Used by Puppet's automated
# serialization routines.
#
# @see Puppet::Network::FormatSupport#to_data_hash
def to_hash
@data
end
alias :to_data_hash :to_hash
def to_json
data = @data.dup.merge('dependencies' => dependencies)
contents = data.keys.map do |k|
value = (Puppet::Util::Json.dump(data[k], :pretty => true) rescue data[k].to_json)
%Q("#{k}": #{value})
end
"{\n" + contents.join(",\n").gsub(/^/, ' ') + "\n}\n"
end
# Expose any metadata keys as callable reader methods.
def method_missing(name, *args)
return @data[name.to_s] if @data.key? name.to_s
super
end
private
# Do basic validation and parsing of the name parameter.
def process_name(data)
validate_name(data['name'])
author, @module_name = data['name'].split(/[-\/]/, 2)
data['author'] ||= author if @data['author'] == DEFAULTS['author']
end
# Do basic validation on the version parameter.
def process_version(data)
validate_version(data['version'])
end
def process_data_provider(data)
validate_data_provider(data['data_provider'])
end
# Do basic parsing of the source parameter. If the source is hosted on
# GitHub, we can predict sensible defaults for both project_page and
# issues_url.
def process_source(data)
if data['source'] =~ %r[://]
source_uri = URI.parse(data['source'])
else
source_uri = URI.parse("http://#{data['source']}")
end
if source_uri.host =~ /^(www\.)?github\.com$/
source_uri.scheme = 'https'
source_uri.path.sub!(/\.git$/, '')
data['project_page'] ||= @data['project_page'] || source_uri.to_s
data['issues_url'] ||= @data['issues_url'] || source_uri.to_s.sub(/\/*$/, '') + '/issues'
end
rescue URI::Error
return
end
# Validates and parses the dependencies.
def merge_dependencies(data)
data['dependencies'].each do |dep|
add_dependency(dep['name'], dep['version_requirement'], dep['repository'])
end
# Clear dependencies so @data dependencies are not overwritten
data.delete 'dependencies'
end
# Validates that the given module name is both namespaced and well-formed.
def validate_name(name)
return if name =~ /\A[a-z0-9]+[-\/][a-z][a-z0-9_]*\Z/i
namespace, modname = name.split(/[-\/]/, 2)
modname = :namespace_missing if namespace == ''
err = case modname
when nil, '', :namespace_missing
_("the field must be a namespaced module name")
when /[^a-z0-9_]/i
_("the module name contains non-alphanumeric (or underscore) characters")
when /^[^a-z]/i
_("the module name must begin with a letter")
else
_("the namespace contains non-alphanumeric characters")
end
raise ArgumentError, _("Invalid 'name' field in metadata.json: %{err}") % { err: err }
end
# Validates that the version string can be parsed as per SemVer.
def validate_version(version)
return if SemanticPuppet::Version.valid?(version)
err = _("version string cannot be parsed as a valid Semantic Version")
raise ArgumentError, _("Invalid 'version' field in metadata.json: %{err}") % { err: err }
end
# Validates that the given _value_ is a symbolic name that starts with a letter
# and then contains only letters, digits, or underscore. Will raise an ArgumentError
# if that's not the case.
#
# @param value [Object] The value to be tested
def validate_data_provider(value)
if value.is_a?(String)
unless value =~ /^[a-zA-Z][a-zA-Z0-9_]*$/
if value =~ /^[a-zA-Z]/
raise ArgumentError, _("field 'data_provider' contains non-alphanumeric characters")
else
raise ArgumentError, _("field 'data_provider' must begin with a letter")
end
end
else
raise ArgumentError, _("field 'data_provider' must be a string")
end
end
# Validates that the version range can be parsed by Semantic.
def validate_version_range(version_range)
SemanticPuppet::VersionRange.parse(version_range)
rescue ArgumentError => e
raise ArgumentError, _("Invalid 'version_range' field in metadata.json: %{err}") % { err: e }
end
end
end