Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature: local cache #35

Merged
merged 21 commits into from Sep 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2eee140
feat(cache): add simple local cache support
icyleaf Aug 30, 2018
037cc11
Refactor(feature): remove Halite::Features module and move logger fea…
icyleaf Aug 31, 2018
fd1402f
refactor(logger): Renamed feature logger to logging to better accept …
icyleaf Aug 31, 2018
8f06c87
refactor(mime_type): Rename Halite::MimeTypes to Halite::MimeType, an…
icyleaf Aug 31, 2018
c2d997d
refactor(mime_type): Rename Halite::MimeTypes to Halite::MimeType, an…
icyleaf Aug 31, 2018
f62437e
fix(logging): Remove instance Logging with format
icyleaf Aug 31, 2018
44c8f44
feat(logging): apply skip_request_body, skip_response_body, skip_benc…
icyleaf Aug 31, 2018
478f087
test: fix ci
icyleaf Aug 31, 2018
72880e1
doc: update new changes in README
icyleaf Aug 31, 2018
655ad4d
chore: clean up
icyleaf Aug 31, 2018
8842c48
fix(cache): keep with new feature spec
icyleaf Aug 31, 2018
7f99953
feat(cache): store cache into a directory with metdata file and body …
icyleaf Aug 31, 2018
195d5ac
doc: update cache comment
icyleaf Aug 31, 2018
7e17d02
fix merge conflict
icyleaf Aug 31, 2018
990b3bd
test: add cache feature spec
icyleaf Aug 31, 2018
229486f
test(cache): add more feature cache tests
icyleaf Sep 3, 2018
c1a4555
test(cache): remove path writable test
icyleaf Sep 3, 2018
f220ba4
feat(cache): load cache from file support
icyleaf Sep 3, 2018
266adb4
Merge branch 'fix/reduce-response-when-use-intercept' into feature/lo…
icyleaf Sep 3, 2018
35563f2
fix(cache): expires compare use utc time
icyleaf Sep 3, 2018
fe4b5b7
refactor(cache): cache default path change to /tmp/halite/cache
icyleaf Sep 3, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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