diff --git a/lib/vagrant/box_metadata.rb b/lib/vagrant/box_metadata.rb new file mode 100644 index 000000000..fead0c0ba --- /dev/null +++ b/lib/vagrant/box_metadata.rb @@ -0,0 +1,124 @@ +require "json" + +module Vagrant + # BoxMetadata represents metadata about a box, including the name + # it should have, a description of it, the versions it has, and + # more. + class BoxMetadata + # The name that the box should be if it is added. + # + # @return [String] + attr_accessor :name + + # The long-form human-readable description of a box. + # + # @return [String] + attr_accessor :description + + # Loads the metadata associated with the box from the given + # IO. + # + # @param [IO] io An IO object to read the metadata from. + def initialize(io) + begin + @raw = JSON.load(io) + rescue JSON::ParserError => e + raise Errors::BoxMetadataMalformed, + error: e.to_s + end + + @name = @raw["name"] + @description = @raw["description"] + @version_map = @raw["versions"].map do |v| + [Gem::Version.new(v["version"]), v] + end + @version_map = Hash[@version_map] + + # TODO: check for corruption: + # - malformed version + end + + # Returns data about a single version that is included in this + # metadata. + # + # @param [String] version The version to return, this can also + # be a constraint. + # @return [Version] The matching version or nil if a matching + # version was not found. + def version(version) + requirements = version.split(",").map do |v| + Gem::Requirement.new(v.strip) + end + + @version_map.keys.sort.reverse.each do |v| + if requirements.all? { |r| r.satisfied_by?(v) } + return Version.new(@version_map[v]) + end + end + + nil + end + + # Returns all the versions supported by this metadata. These + # versions are sorted so the last element of the list is the + # latest version. + # + # @return[Array] + def versions + @version_map.keys.sort.map(&:to_s) + end + + # Represents a single version within the metadata. + class Version + # The version that this Version object represents. + # + # @return [String] + attr_accessor :version + + def initialize(raw=nil) + return if !raw + + @version = raw["version"] + @provider_map = (raw["providers"] || []).map do |p| + [p["name"], p] + end + @provider_map = Hash[@provider_map] + end + + # Returns a [Provider] for the given name, or nil if it isn't + # supported by this version. + def provider(name) + p = @provider_map[name] + return nil if !p + Provider.new(p) + end + + # Returns the providers that are available for this version + # of the box. + # + # @return [Provider] + def providers + @provider_map.keys + end + end + + # Provider represents a single provider-specific box available + # for a version for a box. + class Provider + # The name of the provider. + # + # @return [String] + attr_accessor :name + + # The URL of the box. + # + # @return [String] + attr_accessor :url + + def initialize(raw) + @name = raw["name"] + @url = raw["url"] + end + end + end +end diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 5927c5bfd..099c925a4 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -144,6 +144,10 @@ module Vagrant error_key(:box_metadata_file_not_found) end + class BoxMetadataMalformed < VagrantError + error_key(:box_metadata_malformed) + end + class BoxNotFound < VagrantError error_key(:box_not_found) end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index f0803b809..dbc2eedfa 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -266,6 +266,12 @@ en: box file format can be found at the URL below: http://docs.vagrantup.com/v2/boxes/format.html + box_metadata_malformed: |- + The metadata for the box was malformed. The exact error + is shown below. Please contact the maintainer of the box so + that this issue can be fixed. + + %{error} box_not_found: Box '%{name}' with '%{provider}' provider could not be found. box_provider_doesnt_match: |- The box you attempted to add doesn't match the provider you specified. diff --git a/test/unit/vagrant/box_metadata_test.rb b/test/unit/vagrant/box_metadata_test.rb new file mode 100644 index 000000000..a950168f7 --- /dev/null +++ b/test/unit/vagrant/box_metadata_test.rb @@ -0,0 +1,139 @@ +require File.expand_path("../../base", __FILE__) + +require "vagrant/box_metadata" + +describe Vagrant::BoxMetadata do + include_context "unit" + + let(:raw) do + <<-RAW + { + "name": "foo", + "description": "bar", + "versions": [ + { + "version": "1.0.0" + }, + { + "version": "1.1.5" + }, + { + "version": "1.1.0" + } + ] + } + RAW + end + + subject { described_class.new(raw) } + + its(:name) { should eq("foo") } + its(:description) { should eq("bar") } + + context "with poorly formatted JSON" do + let(:raw) { + <<-RAW + { "name": "foo", } + RAW + } + + it "raises an exception" do + expect { subject }. + to raise_error(Vagrant::Errors::BoxMetadataMalformed) + end + end + + describe "#version" do + it "matches an exact version" do + result = subject.version("1.0.0") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + + it "matches a constraint with latest matching version" do + result = subject.version(">= 1.0") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.1.5") + end + + it "matches complex constraints" do + result = subject.version(">= 0.9, ~> 1.0.0") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + end + + describe "#versions" do + it "returns the versions it contained" do + expect(subject.versions).to eq( + ["1.0.0", "1.1.0", "1.1.5"]) + end + end +end + +describe Vagrant::BoxMetadata::Version do + let(:raw) { {} } + + subject { described_class.new(raw) } + + before do + raw["providers"] = [ + { + "name" => "virtualbox", + }, + { + "name" => "vmware", + } + ] + end + + describe "#version" do + it "is the version in the raw data" do + v = "1.0" + raw["version"] = v + expect(subject.version).to eq(v) + end + end + + describe "#provider" do + it "returns nil if a provider isn't supported" do + expect(subject.provider("foo")).to be_nil + end + + it "returns the provider specified" do + result = subject.provider("virtualbox") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Provider) + end + end + + describe "#providers" do + it "returns the providers available" do + expect(subject.providers.sort).to eq( + ["virtualbox", "vmware"]) + end + end +end + +describe Vagrant::BoxMetadata::Provider do + let(:raw) { {} } + + subject { described_class.new(raw) } + + describe "#name" do + it "is the name specified" do + raw["name"] = "foo" + expect(subject.name).to eq("foo") + end + end + + describe "#url" do + it "is the URL specified" do + raw["url"] = "bar" + expect(subject.url).to eq("bar") + end + end +end