Skip to content

Commit

Permalink
Merge pull request #35 from icyleaf/feature/local-cache
Browse files Browse the repository at this point in the history
New feature: local cache
  • Loading branch information
icyleaf committed Sep 3, 2018
2 parents 12d8aca + fe4b5b7 commit b36866b
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -11,3 +11,4 @@ main.cr
# Dependencies will be locked in application that uses them
shard.lock
.history/
cache/
2 changes: 1 addition & 1 deletion shard.yml
Expand Up @@ -4,6 +4,6 @@ version: 0.6.0
authors:
- icyleaf <icyleaf.cn@gmail.com>

crystal: 0.26.0
crystal: 0.26.1

license: MIT
1 change: 1 addition & 0 deletions spec/fixtures/cache_file.json
@@ -0,0 +1 @@
{"name":"foo3"}
187 changes: 187 additions & 0 deletions spec/halite/features/cache_spec.cr
@@ -0,0 +1,187 @@
require "../../spec_helper"

private struct CacheStruct
getter metadata, body, chain
def initialize(@body : String, @chain : Halite::Feature::Chain, @metadata : Hash(String, JSON::Any)? = nil)
end
end

private def cache_spec(cache, request, response, use_cache = false, wait_time : (Int32 | Time::Span)? = nil)
FileUtils.rm_rf(cache.path)

_chain = Halite::Feature::Chain.new(request, nil, Halite::Options.new) do
response
end

if file = cache.file
chain = cache.intercept(_chain)
body = File.read_lines(file).join("\n")
yield CacheStruct.new(body, chain)
else
key = Digest::MD5.hexdigest("#{request.verb}-#{request.uri}-#{request.body}")
path = File.join(cache.path, key)
metadata_file = File.join(path, "metadata.json")
body_file = File.join(path, "#{key}.cache")

cache.intercept(_chain) if use_cache

if seconds = wait_time
sleep seconds
end

chain = cache.intercept(_chain)

Dir.exists?(path).should be_true
File.file?(metadata_file).should be_true
File.file?(body_file).should be_true

metadata = JSON.parse(File.open(metadata_file)).as_h
body = File.read_lines(body_file).join("\n")

yield CacheStruct.new(body, chain, metadata)

FileUtils.rm_rf(cache.path)
end
end

describe Halite::Cache do
it "should register a format" do
Halite.has_feature?("cache").should be_true
Halite.feature("cache").should eq(Halite::Cache)
end

describe "getters" do
it "should default value" do
feature = Halite::Cache.new
feature.path.should eq(Halite::Cache::DEFAULT_PATH)
feature.expires.should be_nil
feature.debug.should be_true
end

it "should return setter value" do
feature = Halite::Cache.new(path: "/tmp/cache", expires: 1.day, debug: false)
feature.file.should be_nil
feature.path.should eq("/tmp/cache")
feature.expires.should eq(1.day)
feature.debug.should be_false

# expires accept Int32/Time::Span but return Time::Span
feature = Halite::Cache.new(expires: 60)
feature.file.should be_nil
feature.path.should eq(Halite::Cache::DEFAULT_PATH)
feature.expires.should eq(1.minutes)
feature.debug.should be_true
end
end

describe "intercept" do
it "should cache to local storage" do
body = {name: "foo"}.to_json
request = Halite::Request.new("get", SERVER.api("/anything?q=halite1#result"), HTTP::Headers{"Accept" => "application/json"})
response = Halite::Response.new(request.uri, 200, body, HTTP::Headers{"Content-Type" => "application/json", "Content-Length" => body.size.to_s})
feature = Halite::Cache.new
feature.file.should be_nil
feature.path.should eq(Halite::Cache::DEFAULT_PATH)
feature.expires.should be_nil
feature.debug.should be_true

# First return response on HTTP
cache_spec(feature, request, response, use_cache: false) do |result|
result.metadata.not_nil!["status_code"].should eq(200)
result.metadata.not_nil!["headers"].as_h["Content-Type"].should eq("application/json")
result.metadata.not_nil!["headers"].as_h["Content-Length"].should eq(response.body.size.to_s)
result.body.should eq(response.body)
result.chain.result.should eq(Halite::Feature::Chain::Result::Return)
result.chain.response.should eq(response)
end

# Second return response on Cache
cache_spec(feature, request, response, use_cache: true) do |result|
result.metadata.not_nil!["status_code"].should eq(200)
result.metadata.not_nil!["headers"].as_h["Content-Type"].should eq("application/json")
result.metadata.not_nil!["headers"].as_h["Content-Length"].should eq(response.body.size.to_s)
result.body.should eq(response.body)
result.chain.result.should eq(Halite::Feature::Chain::Result::Return)

result.chain.response.should_not be_nil
result.chain.response.not_nil!.headers["X-Cached-From"].should eq("cache")
result.chain.response.not_nil!.headers["X-Cached-Key"].should_not eq("")
result.chain.response.not_nil!.headers["X-Cached-At"].should_not eq("")
result.chain.response.not_nil!.headers["X-Cached-Expires-At"].should eq("None")
end
end

it "should cache without debug mode" do
body = {name: "foo1"}.to_json
request = Halite::Request.new("get", SERVER.api("/anything?q=halite2#result"), HTTP::Headers{"Accept" => "application/json"})
response = Halite::Response.new(request.uri, 200, body, HTTP::Headers{"Content-Type" => "application/json", "Content-Length" => body.size.to_s})
feature = Halite::Cache.new(debug: false)
feature.file.should be_nil
feature.path.should eq(Halite::Cache::DEFAULT_PATH)
feature.expires.should be_nil
feature.debug.should be_false

cache_spec(feature, request, response, use_cache: true) do |result|
result.metadata.not_nil!["status_code"].should eq(200)
result.metadata.not_nil!["headers"].as_h["Content-Type"].should eq("application/json")
result.metadata.not_nil!["headers"].as_h["Content-Length"].should eq(response.body.size.to_s)
result.body.should eq(response.body)
result.chain.result.should eq(Halite::Feature::Chain::Result::Return)

result.chain.response.should_not be_nil
result.chain.response.not_nil!.headers.has_key?("X-Cached-From").should be_false
result.chain.response.not_nil!.headers.has_key?("X-Cached-Key").should be_false
result.chain.response.not_nil!.headers.has_key?("X-Cached-At").should be_false
end
end

it "should return no cache if expired" do
body = {name: "foo2"}.to_json
request = Halite::Request.new("get", SERVER.api("/anything?q=halite3#result"), HTTP::Headers{"Accept" => "application/json"})
response = Halite::Response.new(request.uri, 200, body, HTTP::Headers{"Content-Type" => "application/json", "Content-Length" => body.size.to_s})
feature = Halite::Cache.new(expires: 1.milliseconds)
feature.file.should be_nil
feature.path.should eq(Halite::Cache::DEFAULT_PATH)
feature.expires.should eq(1.milliseconds)
feature.debug.should be_true

cache_spec(feature, request, response, use_cache: true, wait_time: 500.milliseconds) do |result|
result.metadata.not_nil!["status_code"].should eq(200)
result.metadata.not_nil!["headers"].as_h["Content-Type"].should eq("application/json")
result.metadata.not_nil!["headers"].as_h["Content-Length"].should eq(response.body.size.to_s)
result.body.should eq(response.body)
result.chain.result.should eq(Halite::Feature::Chain::Result::Return)

result.chain.response.should_not be_nil
result.chain.response.not_nil!.headers.has_key?("X-Cached-From").should be_false
result.chain.response.not_nil!.headers.has_key?("X-Cached-Key").should be_false
result.chain.response.not_nil!.headers.has_key?("X-Cached-At").should be_false
result.chain.response.not_nil!.headers.has_key?("X-Cached-Expires-At").should be_false
end
end

it "should load cache from file" do
file = fixture_path("cache_file.json")
body = load_fixture("cache_file.json")
request = Halite::Request.new("get", SERVER.api("/anything?q=halite4#result"), HTTP::Headers{"Accept" => "application/json"})
response = Halite::Response.new(request.uri, 200, body, HTTP::Headers{"Content-Type" => "application/json", "Content-Length" => body.size.to_s})
feature = Halite::Cache.new(file: file)
feature.file.should eq(file)
feature.path.should eq(Halite::Cache::DEFAULT_PATH)
feature.expires.should be_nil
feature.debug.should be_true

cache_spec(feature, request, response, file) do |result|
result.metadata.should be_nil
result.body.should eq(response.body)
result.chain.result.should eq(Halite::Feature::Chain::Result::Return)

result.chain.response.should_not be_nil
result.chain.response.not_nil!.headers["X-Cached-From"].should eq("file")
result.chain.response.not_nil!.headers.has_key?("X-Cached-Key").should be_false
result.chain.response.not_nil!.headers["X-Cached-At"].should_not eq("")
result.chain.response.not_nil!.headers.has_key?("X-Cached-Expires-At").should be_false
end
end
end
end
8 changes: 8 additions & 0 deletions spec/spec_helper.cr
Expand Up @@ -66,6 +66,14 @@ class SimpleLogger < Halite::Logging::Abstract
Halite::Logging.register "simple", self
end

def fixture_path(file)
File.join(File.dirname(__FILE__), "fixtures", file)
end

def load_fixture(file)
File.read_lines(fixture_path(file)).join("\n")
end

####################
# Start mock server
####################
Expand Down

0 comments on commit b36866b

Please sign in to comment.