diff --git a/lib/gemstash/cli/base.rb b/lib/gemstash/cli/base.rb index 8e580689..8c6a2254 100644 --- a/lib/gemstash/cli/base.rb +++ b/lib/gemstash/cli/base.rb @@ -35,7 +35,9 @@ def store_config def check_gemstash_version version = Gem::Version.new(Gemstash::Storage.metadata[:gemstash_version]) return if Gem::Requirement.new("<= #{Gemstash::VERSION}").satisfied_by?(Gem::Version.new(version)) - raise Gemstash::CLI::Error.new(@cli, "Gemstash version is too old") + raise Gemstash::CLI::Error.new(@cli, "Gemstash version #{Gemstash::VERSION} does not support version " \ + "#{version}.\nIt appears you may have downgraded Gemstash, please " \ + "install version #{version} or later.") end def pidfile_args diff --git a/lib/gemstash/gem_source/private_source.rb b/lib/gemstash/gem_source/private_source.rb index b53b6e47..aef671cf 100644 --- a/lib/gemstash/gem_source/private_source.rb +++ b/lib/gemstash/gem_source/private_source.rb @@ -71,7 +71,7 @@ def serve_marshal(id) gem = fetch_gem(gem_full_name) halt 404 unless gem.exist?(:spec) content_type "application/octet-stream" - gem.load(:spec).content(:spec) + gem.content(:spec) end def serve_actual_gem(id) @@ -130,7 +130,6 @@ def storage def fetch_gem(gem_full_name) gem = storage.resource(gem_full_name) halt 404 unless gem.exist?(:gem) - gem.load(:gem) halt 403, "That gem has been yanked" unless gem.properties[:indexed] gem end diff --git a/lib/gemstash/gem_source/upstream_source.rb b/lib/gemstash/gem_source/upstream_source.rb index 623908b7..684ea054 100644 --- a/lib/gemstash/gem_source/upstream_source.rb +++ b/lib/gemstash/gem_source/upstream_source.rb @@ -118,10 +118,10 @@ def serve_gem(id) private - def serve_cached(id, key) - gem = fetch_gem(id, key) - headers.update(gem.properties[:headers][key]) if gem.properties[:headers] && gem.properties[:headers][key] - gem.content(key) + def serve_cached(id, resource_type) + gem = fetch_gem(id, resource_type) + headers.update(gem.properties[:headers][resource_type]) if gem.property?(:headers, resource_type) + gem.content(resource_type) rescue Gemstash::WebError => e halt e.code end @@ -142,25 +142,25 @@ def gem_fetcher @gem_fetcher ||= Gemstash::GemFetcher.new(http_client_for(upstream)) end - def fetch_gem(id, key) + def fetch_gem(id, resource_type) gem_name = Gemstash::Upstream::GemName.new(upstream, id) gem_resource = storage.resource(gem_name.name) - if gem_resource.exist?(key) - fetch_local_gem(gem_name, gem_resource, key) + if gem_resource.exist?(resource_type) + fetch_local_gem(gem_name, gem_resource, resource_type) else - fetch_remote_gem(gem_name, gem_resource, key) + fetch_remote_gem(gem_name, gem_resource, resource_type) end end - def fetch_local_gem(gem_name, gem_resource, key) - log.info "Gem #{gem_name.name} exists, returning cached #{key}" - gem_resource.load(key) + def fetch_local_gem(gem_name, gem_resource, resource_type) + log.info "Gem #{gem_name.name} exists, returning cached #{resource_type}" + gem_resource end - def fetch_remote_gem(gem_name, gem_resource, key) - log.info "Gem #{gem_name.name} is not cached, fetching #{key}" - gem_fetcher.fetch(gem_name.id, key) do |content, properties| - gem_resource.save({ key => content }, headers: { key => properties }) + def fetch_remote_gem(gem_name, gem_resource, resource_type) + log.info "Gem #{gem_name.name} is not cached, fetching #{resource_type}" + gem_fetcher.fetch(gem_name.id, resource_type) do |content, properties| + gem_resource.save({ resource_type => content }, headers: { resource_type => properties }) end end end diff --git a/lib/gemstash/storage.rb b/lib/gemstash/storage.rb index 5c056432..1e8cbaab 100644 --- a/lib/gemstash/storage.rb +++ b/lib/gemstash/storage.rb @@ -5,34 +5,57 @@ require "yaml" module Gemstash - #:nodoc: + # The entry point into the storage engine for storing cached gems, specs, and + # private gems. class Storage extend Gemstash::Env::Helper VERSION = 1 - # If the storage engine detects something that was stored with a newer - # version of the storage engine, this error will be thrown. + # If the storage engine detects the base cache directory was originally + # initialized with a newer version, this error is thrown. class VersionTooNew < StandardError + def initialize(folder, version) + super("Gemstash storage version #{Gemstash::Storage::VERSION} does " \ + "not support version #{version} found at #{folder}") + end end + # This object should not be constructed directly, but instead via + # {for} and {#for}. def initialize(folder, root: true) - check_engine if root @folder = folder + check_storage_version if root FileUtils.mkpath(@folder) unless Dir.exist?(@folder) end + # Fetch the resource with the given +id+ within this storage. + # + # @param id [String] the id of the resource to fetch + # @return [Gemstash::Resource] a new resource instance from the +id+ def resource(id) Resource.new(@folder, id) end + # Fetch a nested entry from this instance in the storage engine. + # + # @param child [String] the name of the nested entry to load + # @return [Gemstash::Storage] a new storage instance for the +child+ def for(child) Storage.new(File.join(@folder, child), root: false) end + # Fetch a base entry in the storage engine. + # + # @param name [String] the name of the entry to load + # @return [Gemstash::Storage] a new storage instance for the +name+ def self.for(name) new(gemstash_env.base_file(name)) end + # Read the global metadata for Gemstash and the storage engine. If the + # metadata hasn't been stored yet, it will be created. + # + # @return [Hash] the metadata about Gemstash and the storage engine def self.metadata file = gemstash_env.base_file("metadata.yml") @@ -46,10 +69,10 @@ def self.metadata private - def check_engine + def check_storage_version version = Gemstash::Storage.metadata[:storage_version] return if version <= Gemstash::Storage::VERSION - raise Gemstash::Storage::VersionTooNew, "Storage engine is out of date: #{version}" + raise Gemstash::Storage::VersionTooNew.new(@folder, version) end def path_valid?(path) @@ -59,10 +82,25 @@ def path_valid?(path) end end - #:nodoc: + # A resource within the storage engine. The resource may have 1 or more files + # associated with it along with a metadata Hash that is stored in a YAML file. class Resource include Gemstash::Logging attr_reader :name, :folder + VERSION = 1 + + # If the storage engine detects a resource was originally saved from a newer + # version, this error is thrown. + class VersionTooNew < StandardError + def initialize(name, folder, version) + super("Gemstash resource version #{Gemstash::Resource::VERSION} does " \ + "not support version #{version} for resource #{name.inspect} " \ + "found at #{folder}") + end + end + + # This object should not be constructed directly, but instead via + # {Gemstash::Storage#resource}. def initialize(folder, name) @base_path = folder @name = name @@ -78,6 +116,13 @@ def initialize(folder, name) @folder = File.join(@base_path, *trie_parents, child_folder) end + # When +key+ is nil, this will test if this resource exists with any + # content. If a +key+ is provided, this will test that the resource exists + # with at least the given +key+ file. The +key+ corresponds to the +content+ + # key provided to {#save}. + # + # @param key [Symbol, nil] the key of the content to check existence + # @return [Boolean] true if the indicated content exists def exist?(key = nil) if key File.exist?(properties_filename) && File.exist?(content_filename(key)) @@ -86,6 +131,25 @@ def exist?(key = nil) end end + # Save one or more files for this resource given by the +content+ hash. + # Metadata properties about the file(s) may be provided in the optional + # +properties+ parameter. The keys in the content hash correspond to the + # file name for this resource, while the values will be the content stored + # for that key. + # + # Separate calls to save for the same resource will replace existing files, + # and add new ones. Properties on additional calls will be merged with + # existing properties. + # + # Examples: + # + # Gemstash::Storage.for("foo").resource("bar").save(baz: "qux") + # Gemstash::Storage.for("foo").resource("bar").save(baz: "one", qux: "two") + # Gemstash::Storage.for("foo").resource("bar").save({ baz: "qux" }, meta: true) + # + # @param content [Hash{Symbol => String}] files to save, *must not be nil* + # @param properties [Hash, nil] metadata properties related to this resource + # @return [Gemstash::Resource] self for chaining purposes def save(content, properties = nil) content.each do |key, value| save_content(key, value) @@ -95,28 +159,74 @@ def save(content, properties = nil) self end + # Fetch the content for the given +key+. This will load and cache the + # properties and the content of the +key+. The +key+ corresponds to the + # +content+ key provided to {#save}. + # + # @param key [Symbol] the key of the content to load + # @return [String] the content stored in the +key+ def content(key) + @content ||= {} + load(key) unless @content.include?(key) @content[key] end + # Fetch the metadata properties for this resource. The properties will be + # cached for future calls. + # + # @return [Hash] the metadata properties for this resource def properties + load_properties @properties || {} end + # Update the metadata properties of this resource. The +props+ will be + # merged with any existing properties. + # + # @param props [Hash] the properties to add + # @return [Gemstash::Resource] self for chaining purposes def update_properties(props) - load_properties + load_properties(true) save_properties(properties.merge(props || {})) self end - def load(key) - raise "Resource #{@name} has no content to load" unless exist?(key) - load_properties - @content ||= {} - @content[key] = read_file(content_filename(key)) - self + # Check if the metadata properties includes the +keys+. The +keys+ represent + # a nested path in the properties to check. + # + # Examples: + # + # resource = Gemstash::Storage.for("x").resource("y") + # resource.save({ file: "content" }, foo: "one", bar: { baz: "qux" }) + # resource.has_property?(:foo) # true + # resource.has_property?(:bar, :baz) # true + # resource.has_property?(:missing) # false + # resource.has_property?(:foo, :bar) # false + # + # @param keys [Array] one or more keys pointing to a property + # @return [Boolean] whether the nested keys points to a valid property + def property?(*keys) + keys.inject(node: properties, result: true) do |memo, key| + if memo[:result] + memo[:result] = memo[:node].is_a?(Hash) && memo[:node].include?(key) + memo[:node] = memo[:node][key] if memo[:result] + end + + memo + end[:result] end + # Delete the content for the given +key+. If the +key+ is the last one for + # this resource, the metadata properties will be deleted as well. The +key+ + # corresponds to the +content+ key provided to {#save}. + # + # The resource will be reset afterwards, clearing any cached content or + # properties. + # + # Does nothing if the key doesn't {#exist?}. + # + # @param key [Symbol] the key of the content to delete + # @return [Gemstash::Resource] self for chaining purposes def delete(key) return self unless exist?(key) @@ -139,17 +249,25 @@ def delete(key) private - def load_properties + def load(key) + raise "Resource #{@name} has no #{key.inspect} content to load" unless exist?(key) + load_properties # Ensures storage version is checked + @content ||= {} + @content[key] = read_file(content_filename(key)) + end + + def load_properties(force = false) + return if @properties && !force return unless File.exist?(properties_filename) - @properties = YAML.load_file(properties_filename) - check_version + @properties = YAML.load_file(properties_filename) || {} + check_resource_version end - def check_version - version = @properties[:gemstash_storage_version] - return if version <= Gemstash::Storage::VERSION + def check_resource_version + version = @properties[:gemstash_resource_version] + return if version <= Gemstash::Resource::VERSION reset - raise Gemstash::Storage::VersionTooNew, "Resource was stored with a newer storage: #{version}" + raise Gemstash::Resource::VersionTooNew.new(name, folder, version) end def reset @@ -159,8 +277,8 @@ def reset def content? return false unless Dir.exist?(@folder) - entries = Dir.entries(@folder).reject {|file| file =~ /\A\.\.?\z/ } - !entries.empty? && entries != %w(properties.yaml) + entries = Dir.entries(@folder).reject {|file| file =~ /\A\.\.?\z/ || file == "properties.yaml" } + !entries.empty? end def sanitize(name) @@ -175,7 +293,7 @@ def save_content(key, content) def save_properties(props) props ||= {} - props = { gemstash_storage_version: Gemstash::Storage::VERSION }.merge(props) + props = { gemstash_resource_version: Gemstash::Resource::VERSION }.merge(props) store(properties_filename, props.to_yaml) @properties = props end diff --git a/spec/gemstash/cli/base_spec.rb b/spec/gemstash/cli/base_spec.rb index 19cf6870..d1b237d0 100644 --- a/spec/gemstash/cli/base_spec.rb +++ b/spec/gemstash/cli/base_spec.rb @@ -30,7 +30,7 @@ it "blocks loading when this version is a prerelease of the stored metadata version" do allow(Gemstash::Storage).to receive(:metadata).and_return(gemstash_version: "1.0.0") stub_const("Gemstash::VERSION", "1.0.0.pre.1") - expect { base.send(:check_gemstash_version) }.to raise_error(Gemstash::CLI::Error, /version is too old/) + expect { base.send(:check_gemstash_version) }.to raise_error(Gemstash::CLI::Error, /does not support version/) end it "allows loading when stored metadata is older" do @@ -48,13 +48,13 @@ it "blocks loading when stored metadata is newer" do allow(Gemstash::Storage).to receive(:metadata).and_return(gemstash_version: "1.1.0") stub_const("Gemstash::VERSION", "1.0.0") - expect { base.send(:check_gemstash_version) }.to raise_error(Gemstash::CLI::Error, /version is too old/) + expect { base.send(:check_gemstash_version) }.to raise_error(Gemstash::CLI::Error, /does not support version/) end it "blocks loading when stored metadata is prerelease of a newer version" do allow(Gemstash::Storage).to receive(:metadata).and_return(gemstash_version: "1.1.0.pre.1") stub_const("Gemstash::VERSION", "1.0.0") - expect { base.send(:check_gemstash_version) }.to raise_error(Gemstash::CLI::Error, /version is too old/) + expect { base.send(:check_gemstash_version) }.to raise_error(Gemstash::CLI::Error, /does not support version/) end end end diff --git a/spec/gemstash/gem_pusher_spec.rb b/spec/gemstash/gem_pusher_spec.rb index ce4f207c..549e5480 100644 --- a/spec/gemstash/gem_pusher_spec.rb +++ b/spec/gemstash/gem_pusher_spec.rb @@ -54,12 +54,12 @@ expect(deps.fetch(%w(example))).to eq([]) Gemstash::GemPusher.new(auth_key, gem_contents).push expect(deps.fetch(%w(example))).to match_dependencies(results) - expect(storage.resource("example-0.1.0").load(:gem).content(:gem)).to eq(gem_contents) + expect(storage.resource("example-0.1.0").content(:gem)).to eq(gem_contents) end it "stores the gemspec" do Gemstash::GemPusher.new(auth_key, gem_contents).push - spec = storage.resource("example-0.1.0").load(:spec).content(:spec) + spec = storage.resource("example-0.1.0").content(:spec) spec = Marshal.load(Zlib::Inflate.inflate(spec)) expect(spec).to be_a(Gem::Specification) expect(spec.name).to eq("example") @@ -84,12 +84,12 @@ expect(deps.fetch(%w(example))).to eq([]) Gemstash::GemPusher.new(auth_key, gem_contents).push expect(deps.fetch(%w(example))).to match_dependencies(results) - expect(storage.resource("example-0.1.0-java").load(:gem).content(:gem)).to eq(gem_contents) + expect(storage.resource("example-0.1.0-java").content(:gem)).to eq(gem_contents) end it "stores the gemspec" do Gemstash::GemPusher.new(auth_key, gem_contents).push - spec = storage.resource("example-0.1.0-java").load(:spec).content(:spec) + spec = storage.resource("example-0.1.0-java").content(:spec) spec = Marshal.load(Zlib::Inflate.inflate(spec)) expect(spec).to be_a(Gem::Specification) expect(spec.name).to eq("example") @@ -121,12 +121,12 @@ Gemstash::GemPusher.new(auth_key, gem_contents).push expect(deps.fetch(%w(example))).to match_dependencies(results) - expect(storage.resource("example-0.1.0").load(:gem).content(:gem)).to eq(gem_contents) + expect(storage.resource("example-0.1.0").content(:gem)).to eq(gem_contents) end it "stores the gemspec" do Gemstash::GemPusher.new(auth_key, gem_contents).push - spec = storage.resource("example-0.1.0").load(:spec).content(:spec) + spec = storage.resource("example-0.1.0").content(:spec) spec = Marshal.load(Zlib::Inflate.inflate(spec)) expect(spec).to be_a(Gem::Specification) expect(spec.name).to eq("example") diff --git a/spec/gemstash/gem_unyanker_spec.rb b/spec/gemstash/gem_unyanker_spec.rb index 4ee1c22b..6fb8a7b3 100644 --- a/spec/gemstash/gem_unyanker_spec.rb +++ b/spec/gemstash/gem_unyanker_spec.rb @@ -98,7 +98,7 @@ expect(deps.fetch(%w(example))).to eq([]) Gemstash::GemUnyanker.new(auth_key, gem_name, gem_slug).unyank expect(deps.fetch(%w(example))).to eq([gem_dependencies]) - expect(storage.resource("#{gem_name}-#{gem_version}").load(:gem).content(:gem)).to eq(gem_contents) + expect(storage.resource("#{gem_name}-#{gem_version}").content(:gem)).to eq(gem_contents) end end diff --git a/spec/gemstash/gem_yanker_spec.rb b/spec/gemstash/gem_yanker_spec.rb index 82b47858..1a7b7516 100644 --- a/spec/gemstash/gem_yanker_spec.rb +++ b/spec/gemstash/gem_yanker_spec.rb @@ -89,7 +89,7 @@ Gemstash::GemYanker.new(auth_key, gem_name, gem_slug).yank expect(deps.fetch(%w(example))).to eq([]) # It doesn't actually delete - expect(storage.resource("#{gem_name}-#{gem_version}").load(:gem).content(:gem)).to eq(gem_contents) + expect(storage.resource("#{gem_name}-#{gem_version}").content(:gem)).to eq(gem_contents) end end diff --git a/spec/gemstash/storage_spec.rb b/spec/gemstash/storage_spec.rb index db8032cb..ee8a8c90 100644 --- a/spec/gemstash/storage_spec.rb +++ b/spec/gemstash/storage_spec.rb @@ -32,7 +32,8 @@ } File.write(Gemstash::Env.current.base_file("metadata.yml"), metadata.to_yaml) - expect { Gemstash::Storage.new(@folder) }.to raise_error(Gemstash::Storage::VersionTooNew) + expect { Gemstash::Storage.new(@folder) }. + to raise_error(Gemstash::Storage::VersionTooNew, /#{Regexp.escape(@folder)}/) end context "with a valid storage" do @@ -51,20 +52,20 @@ it "auto sets gemstash version property, even when properties not saved" do resource = storage.resource("something") - resource = resource.save(content: "some content").load(:content) - expect(resource.properties).to eq(gemstash_storage_version: Gemstash::Storage::VERSION) + resource = resource.save(content: "some content") + expect(resource.properties).to eq(gemstash_resource_version: Gemstash::Resource::VERSION) end it "won't update gemstash version when already stored" do - storage.resource("42").save({ content: "content" }, gemstash_storage_version: 0) - expect(storage.resource("42").load(:content).properties[:gemstash_storage_version]).to eq(0) + storage.resource("42").save({ content: "content" }, gemstash_resource_version: 0) + expect(storage.resource("42").properties[:gemstash_resource_version]).to eq(0) storage.resource("42").update_properties(key: "value") - expect(storage.resource("42").load(:content).properties[:gemstash_storage_version]).to eq(0) + expect(storage.resource("42").properties[:gemstash_resource_version]).to eq(0) end it "won't load a resource that is at a larger version than our current version" do - storage.resource("42").save({ content: "content" }, gemstash_storage_version: 999_999) - expect { storage.resource("42").load(:content) }.to raise_error(Gemstash::Storage::VersionTooNew) + storage.resource("42").save({ content: "content" }, gemstash_resource_version: 999_999) + expect { storage.resource("42").content(:content) }.to raise_error(Gemstash::Resource::VersionTooNew, /42/) end context "with a simple resource" do @@ -84,14 +85,14 @@ resource.save({ content: "some other content" }, "content-type" => "octet/stream") expect(resource.content(:content)).to eq("some other content") expect(resource.properties).to eq("content-type" => "octet/stream", - gemstash_storage_version: Gemstash::Storage::VERSION) + gemstash_resource_version: Gemstash::Resource::VERSION) end it "can save nested properties" do resource.save({ content: "some other content" }, headers: { "content-type" => "octet/stream" }) expect(resource.content(:content)).to eq("some other content") expect(resource.properties).to eq(headers: { "content-type" => "octet/stream" }, - gemstash_storage_version: Gemstash::Storage::VERSION) + gemstash_resource_version: Gemstash::Resource::VERSION) end end @@ -104,30 +105,29 @@ it "loads the content from disk" do resource = storage.resource(resource_id) - resource.load(:content) expect(resource.content(:content)).to eq(content) end it "can have properties updated" do resource = storage.resource(resource_id) resource.update_properties(key: "value", other: :value) - expect(storage.resource(resource_id).load(:content).properties). - to eq(key: "value", other: :value, gemstash_storage_version: Gemstash::Storage::VERSION) + expect(storage.resource(resource_id).properties). + to eq(key: "value", other: :value, gemstash_resource_version: Gemstash::Resource::VERSION) resource = storage.resource(resource_id) resource.update_properties(key: "new", new: 42) - expect(storage.resource(resource_id).load(:content).properties). - to eq(key: "new", other: :value, new: 42, gemstash_storage_version: Gemstash::Storage::VERSION) + expect(storage.resource(resource_id).properties). + to eq(key: "new", other: :value, new: 42, gemstash_resource_version: Gemstash::Resource::VERSION) end it "can be deleted" do resource = storage.resource(resource_id) resource.delete(:content) expect(resource.exist?(:content)).to be_falsey - expect { resource.load(:content) }.to raise_error(/no content to load/) + expect { resource.content(:content) }.to raise_error(/no :content content to load/) # Fetching the resource again will still prevent access resource = storage.resource(resource_id) expect(resource.exist?(:content)).to be_falsey - expect { resource.load(:content) }.to raise_error(/no content to load/) + expect { resource.content(:content) }.to raise_error(/no :content content to load/) # Ensure properties is deleted properties_filename = File.join(resource.folder, "properties.yml") @@ -147,7 +147,6 @@ expect(resource.content(:other_content)).to eq(other_content) resource = storage.resource(resource_id) - resource.load(:content).load(:other_content) expect(resource.content(:content)).to eq(content) expect(resource.content(:other_content)).to eq(other_content) end @@ -159,7 +158,6 @@ expect(resource.content(:other_content)).to eq(other_content) resource = storage.resource(resource_id) - resource.load(:content).load(:other_content) expect(resource.content(:content)).to eq(content) expect(resource.content(:other_content)).to eq(other_content) end @@ -167,32 +165,29 @@ it "can be done in 2 saves with separate properties defined" do resource = storage.resource(resource_id) resource.save({ content: content }, foo: "bar").save({ other_content: other_content }, bar: "baz") - expect(resource.properties).to eq(foo: "bar", bar: "baz", gemstash_storage_version: Gemstash::Storage::VERSION) + expect(resource.properties).to eq(foo: "bar", bar: "baz", gemstash_resource_version: Gemstash::Resource::VERSION) resource = storage.resource(resource_id) - resource.load(:content) - expect(resource.properties).to eq(foo: "bar", bar: "baz", gemstash_storage_version: Gemstash::Storage::VERSION) + expect(resource.properties).to eq(foo: "bar", bar: "baz", gemstash_resource_version: Gemstash::Resource::VERSION) end it "can be done in 2 saves with nil properties defined on second" do resource = storage.resource(resource_id) resource.save({ content: content }, foo: "bar").save(other_content: other_content) - expect(resource.properties).to eq(foo: "bar", gemstash_storage_version: Gemstash::Storage::VERSION) + expect(resource.properties).to eq(foo: "bar", gemstash_resource_version: Gemstash::Resource::VERSION) resource = storage.resource(resource_id) - resource.load(:content) - expect(resource.properties).to eq(foo: "bar", gemstash_storage_version: Gemstash::Storage::VERSION) + expect(resource.properties).to eq(foo: "bar", gemstash_resource_version: Gemstash::Resource::VERSION) end it "can be done in 2 saves with separate properties defined from separate resource instances" do storage.resource(resource_id).save({ content: content }, foo: "bar") resource = storage.resource(resource_id) resource.save({ other_content: other_content }, bar: "baz") - expect(resource.properties).to eq(foo: "bar", bar: "baz", gemstash_storage_version: Gemstash::Storage::VERSION) + expect(resource.properties).to eq(foo: "bar", bar: "baz", gemstash_resource_version: Gemstash::Resource::VERSION) resource = storage.resource(resource_id) - resource.load(:content) - expect(resource.properties).to eq(foo: "bar", bar: "baz", gemstash_storage_version: Gemstash::Storage::VERSION) + expect(resource.properties).to eq(foo: "bar", bar: "baz", gemstash_resource_version: Gemstash::Resource::VERSION) end it "supports 1 file being deleted" do @@ -200,12 +195,12 @@ resource = storage.resource(resource_id) resource.delete(:content) expect(resource.exist?(:content)).to be_falsey - expect { resource.load(:content) }.to raise_error(/no content to load/) + expect { resource.content(:content) }.to raise_error(/no :content content to load/) - resource = storage.resource(resource_id).load(:other_content) + resource = storage.resource(resource_id) expect(resource.content(:other_content)).to eq(other_content) - expect(resource.properties).to eq(foo: "bar", gemstash_storage_version: Gemstash::Storage::VERSION) - expect { resource.load(:content) }.to raise_error(/no content to load/) + expect(resource.properties).to eq(foo: "bar", gemstash_resource_version: Gemstash::Resource::VERSION) + expect { resource.content(:content) }.to raise_error(/no :content content to load/) end it "supports both files being deleted" do @@ -215,15 +210,15 @@ expect(resource.exist?(:content)).to be_falsey expect(resource.exist?(:other_content)).to be_falsey expect(resource).to_not exist - expect { resource.load(:content) }.to raise_error(/no content to load/) - expect { resource.load(:other_content) }.to raise_error(/no content to load/) + expect { resource.content(:content) }.to raise_error(/no :content content to load/) + expect { resource.content(:other_content) }.to raise_error(/no :other_content content to load/) resource = storage.resource(resource_id) expect(resource.exist?(:content)).to be_falsey expect(resource.exist?(:other_content)).to be_falsey expect(resource).to_not exist - expect { resource.load(:content) }.to raise_error(/no content to load/) - expect { resource.load(:other_content) }.to raise_error(/no content to load/) + expect { resource.content(:content) }.to raise_error(/no :content content to load/) + expect { resource.content(:other_content) }.to raise_error(/no :other_content content to load/) # Ensure properties is deleted properties_filename = File.join(resource.folder, "properties.yml") @@ -238,8 +233,8 @@ it "stores the content separately" do storage.resource(first_resource_id).save(content: "first content") storage.resource(second_resource_id).save(content: "second content") - expect(storage.resource(first_resource_id).load(:content).content(:content)).to eq("first content") - expect(storage.resource(second_resource_id).load(:content).content(:content)).to eq("second content") + expect(storage.resource(first_resource_id).content(:content)).to eq("first content") + expect(storage.resource(second_resource_id).content(:content)).to eq("second content") end it "uses different downcased paths to avoid issues with case insensitive file systems" do @@ -254,12 +249,78 @@ it "stores and retrieves the data" do storage.resource(resource_id).save(content: "odd name content") - expect(storage.resource(resource_id).load(:content).content(:content)).to eq("odd name content") + expect(storage.resource(resource_id).content(:content)).to eq("odd name content") end it "doesn't include the odd characters in the path" do expect(storage.resource(resource_id).folder).to_not match(/[.=$&]/) end end + + describe "#property?" do + let(:resource) { storage.resource("existing") } + + context "with a single key" do + before do + resource.save({ file: "content" }, foo: "one", bar: nil, baz: { qux: "two" }) + end + + it "returns true for a valid key" do + expect(resource.property?(:foo)).to eq(true) + end + + it "returns true for a key pointing to explicit nil" do + expect(resource.property?(:bar)).to eq(true) + end + + it "returns true for a key pointing to a nested hash" do + expect(resource.property?(:baz)).to eq(true) + end + + it "returns false if the resource doesn't exist" do + expect(storage.resource("missing").property?(:foo)).to eq(false) + end + + it "returns false for a missing key" do + expect(resource.property?(:missing)).to eq(false) + end + end + + context "with nested keys" do + before do + resource.save({ file: "content" }, parent: { foo: "one", bar: nil, baz: { qux: "two" } }) + end + + it "returns true for a valid set of keys" do + expect(resource.property?(:parent, :foo)).to eq(true) + end + + it "returns true for a set of keys pointing to explicit nil" do + expect(resource.property?(:parent, :bar)).to eq(true) + end + + it "returns true for a set of keys pointing to a nested hash" do + expect(resource.property?(:parent, :baz)).to eq(true) + end + + it "returns false if the resource doesn't exist" do + expect(storage.resource("missing").property?(:parent, :foo)).to eq(false) + end + + it "returns false for a missing leaf key" do + expect(resource.property?(:parent, :missing)).to eq(false) + end + + it "returns false for a missing parent key" do + expect(resource.property?(:missing, :foo)).to eq(false) + expect(resource.property?(:parent, :missing)).to eq(false) + end + + it "returns false if a key hits a non-hash" do + expect(resource.property?(:parent, :foo, :non_node)).to eq(false) + expect(resource.property?(:parent, :bar, :non_node)).to eq(false) + end + end + end end end diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 5adb0e38..90abc3d5 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -96,7 +96,7 @@ context "pushing a gem" do before do expect(deps.fetch(%w(speaker))).to match_dependencies([]) - expect { storage.resource("speaker-0.1.0").load(:gem) }.to raise_error(RuntimeError) + expect { storage.resource("speaker-0.1.0").content(:gem) }.to raise_error(RuntimeError) @gemstash.env.cache.flush end @@ -104,7 +104,7 @@ env = { "HOME" => env_dir } expect(execute("gem", ["push", "--key", "test", "--host", host, gem], env: env)).to exit_success expect(deps.fetch(%w(speaker))).to match_dependencies([speaker_deps]) - expect(storage.resource("speaker-0.1.0").load(:gem).content(:gem)).to eq(gem_contents) + expect(storage.resource("speaker-0.1.0").content(:gem)).to eq(gem_contents) expect(http_client.get("gems/speaker-0.1.0")).to eq(gem_contents) end end @@ -121,7 +121,7 @@ expect(execute("gem", ["yank", "--key", "test", gem_name, "--version", gem_version], env: env)).to exit_success expect(deps.fetch(%w(speaker))).to match_dependencies([]) # It shouldn't actually delete the gem, to support unyank - expect(storage.resource("speaker-0.1.0").load(:gem).content(:gem)).to eq(gem_contents) + expect(storage.resource("speaker-0.1.0").content(:gem)).to eq(gem_contents) # But it should block downloading the yanked gem expect { http_client.get("gems/speaker-0.1.0") }.to raise_error(Gemstash::WebError) end @@ -140,7 +140,7 @@ expect(execute("gem", ["yank", "--key", "test", gem_name, "--version", gem_version, "--undo"], env: env)). to exit_success expect(deps.fetch(%w(speaker))).to match_dependencies([speaker_deps]) - expect(storage.resource("speaker-0.1.0").load(:gem).content(:gem)).to eq(gem_contents) + expect(storage.resource("speaker-0.1.0").content(:gem)).to eq(gem_contents) expect(http_client.get("gems/speaker-0.1.0")).to eq(gem_contents) end end