1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
|
# frozen_string_literal: true
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)
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 = begin
Puppet::Util::Json.dump(data[k], :pretty => true)
rescue
data[k].to_json
end
%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(%r{[-/]}, 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(%r{/*$}, '') + '/issues'
end
rescue URI::Error
nil
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 =~ %r{\A[a-z0-9]+[-/][a-z][a-z0-9_]*\Z}i
namespace, modname = name.split(%r{[-/]}, 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
|