From 2eee1409bafb9cc71b7de3b2af5c913dc56ac1b6 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Thu, 30 Aug 2018 18:51:19 +0800 Subject: [PATCH 01/19] feat(cache): add simple local cache support --- .gitignore | 1 + src/halite/features/cache.cr | 109 +++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/halite/features/cache.cr diff --git a/.gitignore b/.gitignore index 28e8ab8a..4d705e82 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ main.cr # Dependencies will be locked in application that uses them shard.lock .history/ +cache/ diff --git a/src/halite/features/cache.cr b/src/halite/features/cache.cr new file mode 100644 index 00000000..9bd0c07f --- /dev/null +++ b/src/halite/features/cache.cr @@ -0,0 +1,109 @@ +require "json" +require "digest" +require "file_utils" + +module Halite::Features + # Cache feature use for caching HTTP response to local storage to speed up in developing stage. + # + # It has the following options: + # + # - `path`: The path of cache, default is "cache/" + # - `expires`: The expires time of cache, default is nerver expires. + # - `debug`: The debug mode of cache, default is `true` + # + # With debug mode, cached response it always included some headers information: + # + # - `X-Cached-Key`: Cache key with verb, uri and body + # - `X-Cached-At`: Cache created time + # - `X-Cached-Expires-At`: Cache expired time + # - `X-Cached-By`: Always return "Halite" + # + # ``` + # Halite.use("cache").get "http://httpbin.org/anything" # request a HTTP + # r = Halite.use("cache").get "http://httpbin.org/anything" # request from local storage + # r.headers # => {..., "X-Cached-At" => "2018-08-30 10:41:14 UTC", "X-Cached-By" => "Halite", "X-Cached-Expires-At" => "2018-08-30 10:41:19 UTC", "X-Cached-Key" => "2bb155e6c8c47627da3d91834eb4249a"}} + # ``` + class Cache < Feature + DEFAULT_PATH = "cache/" + + @path : String + @expires : Time::Span? + @debug : Bool + + def initialize(**options) + @path = options.fetch(:path, DEFAULT_PATH).as(String) + @expires = options[:expires]?.as(Time::Span?) + @debug = options.fetch(:debug, true).as(Bool) + end + + def intercept(chain) + response = cache(chain) do + chain.perform + end + + chain.return(response) + end + + private def cache(chain : Interceptor::Chain, &block : -> Response) + if response = find_cache(chain.request) + return response + end + + response = yield + write_cache(chain.request, response) + response + end + + private def find_cache(request : Request) : Response? + key = generate_cache_key(request) + file = File.join(@path, key) + + if File.exists?(file) && !cache_expired?(file) + cache = JSON.parse(File.open(file)).as_h + status_code = cache["status_code"].as_i + body = cache["body"].as_s + headers = cache["headers"].as_h.each_with_object(HTTP::Headers.new) do |(key, value), obj| + obj[key] = value.as_s + end + + if @debug + headers["X-Cached-Key"] = key + headers["X-Cached-At"] = cache_created_time(file).to_s + headers["X-Cached-Expires-At"] = @expires ? (cache_created_time(file) + @expires.not_nil!).to_s : "None" + headers["X-Cached-By"] = "Halite" + end + + return Response.new(request.uri, status_code, body, headers) + end + end + + private def cache_expired?(file) + return false unless expires = @expires + file_modified_time = cache_created_time(file) + Time.now >= (file_modified_time + expires) + end + + private def cache_created_time(file) + File.info(file).modification_time + end + + private def generate_cache_key(request : Request) : String + Digest::MD5.hexdigest("#{request.verb}-#{request.uri}-#{request.body}") + end + + private def write_cache(request, response) + FileUtils.mkdir_p(@path) unless Dir.exists?(@path) + + key = generate_cache_key(request) + File.open(File.join(@path, key), "w") do |f| + f.puts({ + "status_code" => response.status_code, + "headers" => response.headers.to_h, + "body" => response.body + }.to_json) + end + end + + Halite::Features.register "cache", self + end +end From 037cc1152ebe171ce2e677aad9c5e7b891d1e563 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 11:27:21 +0800 Subject: [PATCH 02/19] Refactor(feature): remove Halite::Features module and move logger feature under Halite and some method changes. Feature: - `Halite::Features::Logger` to `Halite::Logger` - `Halite::Features.register` to `Halite.register_feature` - `Halite::Features[]/[]?` to `Halite.features/features?` - `Halite::Features.avaiables` to `Halite.has_feature?` - `Halite::Feature::Interceptor::Chain` to `Halite::Feature::Chain` --- README.md | 2 +- spec/halite/feature_spec.cr | 10 +++++++++ spec/halite/features/logger_spec.cr | 16 +++++++-------- spec/halite/features_spec.cr | 21 ------------------- spec/halite/options_spec.cr | 28 +++++++++++++------------- spec/halite_spec.cr | 23 ++++++++++++++------- spec/spec_helper.cr | 8 ++++---- src/halite.cr | 22 ++++++++++++++++++++ src/halite/chainable.cr | 6 +++--- src/halite/client.cr | 6 +++--- src/halite/{features.cr => feature.cr} | 27 ++----------------------- src/halite/features/logger.cr | 4 ++-- src/halite/features/loggers/common.cr | 2 +- src/halite/features/loggers/json.cr | 2 +- src/halite/options.cr | 6 +++--- 15 files changed, 90 insertions(+), 93 deletions(-) create mode 100644 spec/halite/feature_spec.cr delete mode 100644 spec/halite/features_spec.cr rename src/halite/{features.cr => feature.cr} (72%) diff --git a/README.md b/README.md index 72ddf2e0..b26432ee 100644 --- a/README.md +++ b/README.md @@ -670,7 +670,7 @@ client.post("http://httpbin.org/post", form: {name: "foo"}) #### Write a interceptor -Halite features has a killer feature is the **interceptor*, Use `Halite::Interceptor::Chain` to process with two result: +Halite features has a killer feature is the **interceptor*, Use `Halite::Feature::Chain` to process with two result: - `next`: perform and run next interceptor - `return`: perform and return diff --git a/spec/halite/feature_spec.cr b/spec/halite/feature_spec.cr new file mode 100644 index 00000000..84c7f7f0 --- /dev/null +++ b/spec/halite/feature_spec.cr @@ -0,0 +1,10 @@ +require "../spec_helper" + +describe Halite::Feature do + it "should a empty feature" do + feature = TestFeatures::Null.new + feature.responds_to?(:request).should be_true + feature.responds_to?(:response).should be_true + feature.responds_to?(:intercept).should be_true + end +end diff --git a/spec/halite/features/logger_spec.cr b/spec/halite/features/logger_spec.cr index 38f1989e..db82e173 100644 --- a/spec/halite/features/logger_spec.cr +++ b/spec/halite/features/logger_spec.cr @@ -1,6 +1,6 @@ require "../../spec_helper" -private class SimpleLogger < Halite::Features::Logger::Abstract +private class SimpleLogger < Halite::Logger::Abstract def request(request) @logger.info "request" end @@ -9,18 +9,18 @@ private class SimpleLogger < Halite::Features::Logger::Abstract @logger.info "response" end - Halite::Features::Logger.register "simple", self + Halite::Logger.register "simple", self end -describe Halite::Features::Logger do +describe Halite::Logger do it "should register an format" do - Halite::Features::Logger["simple"].should eq(SimpleLogger) - Halite::Features::Logger.availables.should eq ["common", "json", "simple"] + Halite::Logger["simple"].should eq(SimpleLogger) + Halite::Logger.availables.should eq ["common", "json", "simple"] end it "should use common as default logger" do - logger = Halite::Features::Logger.new - logger.writer.should be_a(Halite::Features::Logger::Common) + logger = Halite::Logger.new + logger.writer.should be_a(Halite::Logger::Common) logger.writer.skip_request_body.should be_false logger.writer.skip_response_body.should be_false logger.writer.skip_benchmark.should be_false @@ -28,7 +28,7 @@ describe Halite::Features::Logger do end it "should use custom logger" do - logger = Halite::Features::Logger.new(logger: SimpleLogger.new) + logger = Halite::Logger.new(logger: SimpleLogger.new) logger.writer.should be_a(SimpleLogger) logger.writer.skip_request_body.should be_false logger.writer.skip_response_body.should be_false diff --git a/spec/halite/features_spec.cr b/spec/halite/features_spec.cr deleted file mode 100644 index 2eca7180..00000000 --- a/spec/halite/features_spec.cr +++ /dev/null @@ -1,21 +0,0 @@ -require "../spec_helper" - -describe Halite::Features do - describe "register" do - it "should use a registered feature" do - Halite::Features["null"]?.should be_nil - Halite::Features.register "null", TestFeatures::Null - Halite::Features.availables.includes?("null").should be_true - Halite::Features["null"].should eq(TestFeatures::Null) - end - end -end - -describe Halite::Feature do - it "should a empty feature" do - feature = TestFeatures::Null.new - feature.responds_to?(:request).should be_true - feature.responds_to?(:response).should be_true - feature.responds_to?(:intercept).should be_true - end -end diff --git a/spec/halite/options_spec.cr b/spec/halite/options_spec.cr index 6f4197f1..8a877d32 100644 --- a/spec/halite/options_spec.cr +++ b/spec/halite/options_spec.cr @@ -10,10 +10,10 @@ private class SimpleFeature < Halite::Feature response end - Halite::Features.register "simple", self + Halite.register_feature "simple", self end -private class SimpleLogger < Halite::Features::Logger::Abstract +private class SimpleLogger < Halite::Logger::Abstract def request(request) @logger.info "request" end @@ -22,7 +22,7 @@ private class SimpleLogger < Halite::Features::Logger::Abstract @logger.info "response" end - Halite::Features::Logger.register "simple", self + Halite::Logger.register "simple", self end private def test_options @@ -256,25 +256,25 @@ describe Halite::Options do describe "#with_logger" do it "should overwrite logger with instance class" do options = Halite::Options.new.with_logger(logger: SimpleLogger.new) - logger = options.features["logger"].as(Halite::Features::Logger) + logger = options.features["logger"].as(Halite::Logger) logger.writer.should be_a(SimpleLogger) end it "should overwrite logger with format name" do - Halite::Features::Logger.register "simple", SimpleLogger + Halite::Logger.register "simple", SimpleLogger options = Halite::Options.new.with_logger(format: "simple") - logger = options.features["logger"].as(Halite::Features::Logger) + logger = options.features["logger"].as(Halite::Logger) logger.writer.should be_a(SimpleLogger) end it "should became a file logger" do - Halite::Features::Logger.register "simple", SimpleLogger + Halite::Logger.register "simple", SimpleLogger tempfile = Tempfile.new("halite_logger") options = Halite::Options.new.with_logger(format: "simple", file: tempfile.path, filemode: "w") - logger = options.features["logger"].as(Halite::Features::Logger) + logger = options.features["logger"].as(Halite::Logger) logger.writer.should be_a(SimpleLogger) end @@ -288,22 +288,22 @@ describe Halite::Options do describe "#with_features" do it "should use a feature" do options = Halite::Options.new.with_features("logger") - logger = options.features["logger"].as(Halite::Features::Logger) - logger.writer.should be_a(Halite::Features::Logger::Common) + logger = options.features["logger"].as(Halite::Logger) + logger.writer.should be_a(Halite::Logger::Common) end it "should use a feature with options" do options = Halite::Options.new.with_features("logger", logger: SimpleLogger.new) - logger = options.features["logger"].as(Halite::Features::Logger) + logger = options.features["logger"].as(Halite::Logger) logger.writer.should be_a(SimpleLogger) end it "should use multiple features" do - Halite::Features.register "simple", SimpleFeature + Halite.register_feature "simple", SimpleFeature options = Halite::Options.new.with_features("logger", "simple") - logger = options.features["logger"].as(Halite::Features::Logger) - logger.writer.should be_a(Halite::Features::Logger::Common) + logger = options.features["logger"].as(Halite::Logger) + logger.writer.should be_a(Halite::Logger::Common) simple = options.features["simple"].as(SimpleFeature) simple.should be_a(SimpleFeature) diff --git a/spec/halite_spec.cr b/spec/halite_spec.cr index dca0c183..a22a248b 100644 --- a/spec/halite_spec.cr +++ b/spec/halite_spec.cr @@ -313,9 +313,9 @@ describe Halite do it "sets given feature name" do client = Halite.use("logger") client.options.features.has_key?("logger").should be_true - client.options.features["logger"].should be_a(Halite::Features::Logger) - logger = client.options.features["logger"].as(Halite::Features::Logger) - logger.writer.should be_a(Halite::Features::Logger::Common) + client.options.features["logger"].should be_a(Halite::Logger) + logger = client.options.features["logger"].as(Halite::Logger) + logger.writer.should be_a(Halite::Logger::Common) logger.writer.skip_request_body.should be_false logger.writer.skip_response_body.should be_false logger.writer.skip_benchmark.should be_false @@ -323,11 +323,11 @@ describe Halite do end it "sets given feature name and options" do - client = Halite.use("logger", logger: Halite::Features::Logger::JSON.new(skip_request_body: true, colorize: false)) + client = Halite.use("logger", logger: Halite::Logger::JSON.new(skip_request_body: true, colorize: false)) client.options.features.has_key?("logger").should be_true - client.options.features["logger"].should be_a(Halite::Features::Logger) - logger = client.options.features["logger"].as(Halite::Features::Logger) - logger.writer.should be_a(Halite::Features::Logger::JSON) + client.options.features["logger"].should be_a(Halite::Logger) + logger = client.options.features["logger"].as(Halite::Logger) + logger.writer.should be_a(Halite::Logger::JSON) logger.writer.skip_request_body.should be_true logger.writer.skip_response_body.should be_false logger.writer.skip_benchmark.should be_false @@ -393,4 +393,13 @@ describe Halite do end end end + + describe Halite::FeatureRegister do + it "should use a registered feature" do + Halite.feature?("null").should be_nil + Halite.register_feature "null", TestFeatures::Null + Halite.has_feature?("null").should be_true + Halite.feature("null").should eq(TestFeatures::Null) + end + end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index d6080f31..51cb5863 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -16,7 +16,7 @@ module TestFeatures response end - Halite::Features.register "append_headers", self + Halite.register_feature "append_headers", self end end @@ -27,7 +27,7 @@ module TestInterceptors chain.return(response) end - Halite::Features.register "mock", self + Halite.register_feature "mock", self end class AlwaysNotFound < Halite::Feature @@ -37,7 +37,7 @@ module TestInterceptors chain.next(response) end - Halite::Features.register "404", self + Halite.register_feature "404", self end class PoweredBy < Halite::Feature @@ -50,7 +50,7 @@ module TestInterceptors end end - Halite::Features.register "powered_by", self + Halite.register_feature "powered_by", self end end diff --git a/src/halite.cr b/src/halite.cr index 6083b50a..6355a802 100644 --- a/src/halite.cr +++ b/src/halite.cr @@ -3,4 +3,26 @@ require "./halite/ext/*" module Halite extend Chainable + + @@features = {} of String => Feature.class + + module FeatureRegister + def register_feature(name : String, klass : Feature.class) + @@features[name] = klass + end + + def feature(name : String) + @@features[name] + end + + def feature?(name : String) + @@features[name]? + end + + def has_feature?(name) + @@features.keys.includes?(name) + end + end + + extend FeatureRegister end diff --git a/src/halite/chainable.cr b/src/halite/chainable.cr index 7f09bb75..d3bec957 100644 --- a/src/halite/chainable.cr +++ b/src/halite/chainable.cr @@ -260,11 +260,11 @@ module Halite # # #### Use custom logger # - # Creating the custom logger by integration `Halite::Features::Logger::Abstract` abstract class. + # Creating the custom logger by integration `Halite::Logger::Abstract` abstract class. # Here has two methods must be implement: `#request` and `#response`. # # ``` - # class CustomLogger < Halite::Features::Logger::Abstract + # class CustomLogger < Halite::Logger::Abstract # def request(request) # @logger.info "| >> | %s | %s %s" % [request.verb, request.uri, request.body] # end @@ -287,7 +287,7 @@ module Halite # # => 2017-12-13 16:40:13 +08:00 | >> | GET | http://httpbin.org/get?name=foobar # # => 2017-12-13 16:40:15 +08:00 | << | 200 | http://httpbin.org/get?name=foobar application/json # ``` - def logger(logger = Halite::Features::Logger::Common.new) + def logger(logger = Halite::Logger::Common.new) branch(default_options.with_logger(logger)) end diff --git a/src/halite/client.cr b/src/halite/client.cr index 39640d70..fd89bb06 100644 --- a/src/halite/client.cr +++ b/src/halite/client.cr @@ -104,12 +104,12 @@ module Halite # Find interceptor and return `Response` else perform HTTP request. private def perform(request : Halite::Request, options : Halite::Options, &block : -> Response) - chain = Interceptor::Chain.new(request, nil, options, &block) + chain = Feature::Chain.new(request, nil, options, &block) options.features.each do |_, feature| current_chain = feature.intercept(chain) - if current_chain.result == Interceptor::Chain::Result::Next + if current_chain.result == Feature::Chain::Result::Next chain = current_chain - elsif current_chain.result == Interceptor::Chain::Result::Return && (response = current_chain.response) + elsif current_chain.result == Feature::Chain::Result::Return && (response = current_chain.response) return response end end diff --git a/src/halite/features.cr b/src/halite/feature.cr similarity index 72% rename from src/halite/features.cr rename to src/halite/feature.cr index 93496a25..75cdbdce 100644 --- a/src/halite/features.cr +++ b/src/halite/feature.cr @@ -1,24 +1,4 @@ module Halite - module Features - @@features = {} of String => Feature.class - - def self.register(name : String, klass : Feature.class) - @@features[name] = klass - end - - def self.[](name : String) - @@features[name] - end - - def self.[]?(name : String) - @@features[name]? - end - - def self.availables - @@features.keys - end - end - abstract class Feature def initialize(**options) end @@ -34,14 +14,11 @@ module Halite end # Intercept and cooking request and response - def intercept(chain : Interceptor::Chain) : Interceptor::Chain + def intercept(chain : Chain) : Chain chain end - end - # Interceptor for feature - module Interceptor - # Interceptor chain + # Feature chain # # Chain has two result: # diff --git a/src/halite/features/logger.cr b/src/halite/features/logger.cr index a10fc4e5..bf2ddaaf 100644 --- a/src/halite/features/logger.cr +++ b/src/halite/features/logger.cr @@ -2,7 +2,7 @@ require "logger" require "colorize" require "file_utils" -module Halite::Features +module Halite # Logger feature class Logger < Feature def self.new(format : String = "common", logger : Logger::Abstract? = nil, **opts) @@ -93,7 +93,7 @@ module Halite::Features extend Register - Halite::Features.register "logger", self + Halite.register_feature "logger", self end end diff --git a/src/halite/features/loggers/common.cr b/src/halite/features/loggers/common.cr index 9ea354a1..fb3da864 100644 --- a/src/halite/features/loggers/common.cr +++ b/src/halite/features/loggers/common.cr @@ -1,6 +1,6 @@ require "file_utils" -class Halite::Features::Logger +class Halite::Logger # Logger feature: Logger::Common class Common < Abstract @request_time : Time? diff --git a/src/halite/features/loggers/json.cr b/src/halite/features/loggers/json.cr index 12372b39..15209a64 100644 --- a/src/halite/features/loggers/json.cr +++ b/src/halite/features/loggers/json.cr @@ -1,6 +1,6 @@ require "json" -class Halite::Features::Logger +class Halite::Logger # Logger feature: Logger::JSON class JSON < Abstract @created_at : Time? = nil diff --git a/src/halite/options.cr b/src/halite/options.cr index 5afc5d7f..a412d4f9 100644 --- a/src/halite/options.cr +++ b/src/halite/options.cr @@ -174,7 +174,7 @@ module Halite # Returns `Options` self with feature name and options. def with_features(name : String, opts : NamedTuple) - raise UnRegisterFeatureError.new("Not avaiable feature: #{name}") unless klass = Features[name]? + raise UnRegisterFeatureError.new("Not avaiable feature: #{name}") unless klass = Halite.feature?(name) @features[name] = klass.new(**opts) self end @@ -187,12 +187,12 @@ module Halite # Returns `Logger` self with given format and the options of format. def with_logger(format : String, **opts) - raise UnRegisterLoggerFormatError.new("Not avaiable logger format: #{format}") unless format_cls = Features::Logger[format]? + raise UnRegisterLoggerFormatError.new("Not avaiable logger format: #{format}") unless format_cls = Logger[format]? with_logger(format_cls.new(**opts)) end # Returns `Logger` self with given logger, depend on `with_features`. - def with_logger(logger : Halite::Features::Logger::Abstract) + def with_logger(logger : Halite::Logger::Abstract) @logging = true with_features("logger", logger: logger) self From fd1402fef20372d443a165ecc7f2e31ab1dce1d1 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 11:32:28 +0800 Subject: [PATCH 03/19] refactor(logger): Renamed feature logger to logging to better accept logger format --- README.md | 16 +++++----- spec/halite/features/logger_spec.cr | 16 +++++----- spec/halite/options_spec.cr | 30 +++++++++---------- spec/halite_spec.cr | 20 ++++++------- src/halite/chainable.cr | 16 +++++----- src/halite/features/{logger.cr => logging.cr} | 18 +++++------ .../features/{loggers => logging}/common.cr | 6 ++-- .../features/{loggers => logging}/json.cr | 4 +-- src/halite/options.cr | 15 ++++------ 9 files changed, 68 insertions(+), 73 deletions(-) rename src/halite/features/{logger.cr => logging.cr} (81%) rename src/halite/features/{loggers => logging}/common.cr (97%) rename src/halite/features/{loggers => logging}/json.cr (96%) diff --git a/README.md b/README.md index b26432ee..87153d86 100644 --- a/README.md +++ b/README.md @@ -528,7 +528,7 @@ We can enable per operation logging by configuring them through the chaining API By default, Halite will logging all outgoing HTTP requests and their responses(without binary stream) to `STDOUT` on DEBUG level. You can configuring the following options: -- `logger`: Instance your `Halite::Features::Logger::Abstract`, check [Use the custom logger](#use-the-custom-logger). +- `logger`: Instance your `Halite::Logger::Abstract`, check [Use the custom logger](#use-the-custom-logger). - `format`: Outputing format, built-in `common` and `json`, you can write your own. - `file`: Write to file with path, works with `format`. - `filemode`: Write file mode, works with `format`, by default is `a`. (append to bottom, create it if file is not exist) @@ -588,11 +588,11 @@ Halite.logger(format: "json", file: "logs/halite.log") #### Use the custom logger -Creating the custom logger by integration `Halite::Features::Logger::Abstract` abstract class. +Creating the custom logger by integration `Halite::Logger::Abstract` abstract class. Here has two methods must be implement: `#request` and `#response`. ```crystal -class CustomLogger < Halite::Features::Logger::Abstract +class CustomLogger < Halite::Logging::Abstract def request(request) @logger.info "| >> | %s | %s %s" % [request.verb, request.uri, request.body] end @@ -603,7 +603,7 @@ class CustomLogger < Halite::Features::Logger::Abstract end # Add to adapter list (optional) -Halite::Logger.register_adapter "custom", CustomLogger.new +Halite::Logging.register "custom", CustomLogger.new Halite.logger(logger: CustomLogger.new) .get("http://httpbin.org/get", params: {name: "foobar"}) @@ -623,7 +623,7 @@ in your HTTP client and allowing you to monitor outgoing requests, and incoming Avaiabled features: -- logger (Cool, aha!) +- logging (Cool, aha!) #### Write a simple feature @@ -645,7 +645,7 @@ class RequestMonister < Halite::Feature request end - Halite::Features.register "request_monster", self + Halite.register_feature "request_monster", self end ``` @@ -685,7 +685,7 @@ class AlwaysNotFound < Halite::Feature chain.next(response) end - Halite::Features.register "404", self + Halite.register_feature "404", self end class PoweredBy < Halite::Feature @@ -698,7 +698,7 @@ class PoweredBy < Halite::Feature end end - Halite::Features.register "powered_by", self + Halite.register_feature "powered_by", self end r = Halite.use("404").use("powered_by").get("http://httpbin.org/user-agent") diff --git a/spec/halite/features/logger_spec.cr b/spec/halite/features/logger_spec.cr index db82e173..28fe5525 100644 --- a/spec/halite/features/logger_spec.cr +++ b/spec/halite/features/logger_spec.cr @@ -1,6 +1,6 @@ require "../../spec_helper" -private class SimpleLogger < Halite::Logger::Abstract +private class SimpleLogger < Halite::Logging::Abstract def request(request) @logger.info "request" end @@ -9,18 +9,18 @@ private class SimpleLogger < Halite::Logger::Abstract @logger.info "response" end - Halite::Logger.register "simple", self + Halite::Logging.register "simple", self end -describe Halite::Logger do +describe Halite::Logging do it "should register an format" do - Halite::Logger["simple"].should eq(SimpleLogger) - Halite::Logger.availables.should eq ["common", "json", "simple"] + Halite::Logging["simple"].should eq(SimpleLogger) + Halite::Logging.availables.should eq ["common", "json", "simple"] end it "should use common as default logger" do - logger = Halite::Logger.new - logger.writer.should be_a(Halite::Logger::Common) + logger = Halite::Logging.new + logger.writer.should be_a(Halite::Logging::Common) logger.writer.skip_request_body.should be_false logger.writer.skip_response_body.should be_false logger.writer.skip_benchmark.should be_false @@ -28,7 +28,7 @@ describe Halite::Logger do end it "should use custom logger" do - logger = Halite::Logger.new(logger: SimpleLogger.new) + logger = Halite::Logging.new(logger: SimpleLogger.new) logger.writer.should be_a(SimpleLogger) logger.writer.skip_request_body.should be_false logger.writer.skip_response_body.should be_false diff --git a/spec/halite/options_spec.cr b/spec/halite/options_spec.cr index 8a877d32..74cae88c 100644 --- a/spec/halite/options_spec.cr +++ b/spec/halite/options_spec.cr @@ -13,7 +13,7 @@ private class SimpleFeature < Halite::Feature Halite.register_feature "simple", self end -private class SimpleLogger < Halite::Logger::Abstract +private class SimpleLogger < Halite::Logging::Abstract def request(request) @logger.info "request" end @@ -22,7 +22,7 @@ private class SimpleLogger < Halite::Logger::Abstract @logger.info "response" end - Halite::Logger.register "simple", self + Halite::Logging.register "simple", self end private def test_options @@ -256,25 +256,25 @@ describe Halite::Options do describe "#with_logger" do it "should overwrite logger with instance class" do options = Halite::Options.new.with_logger(logger: SimpleLogger.new) - logger = options.features["logger"].as(Halite::Logger) + logger = options.features["logging"].as(Halite::Logging) logger.writer.should be_a(SimpleLogger) end it "should overwrite logger with format name" do - Halite::Logger.register "simple", SimpleLogger + Halite::Logging.register "simple", SimpleLogger options = Halite::Options.new.with_logger(format: "simple") - logger = options.features["logger"].as(Halite::Logger) + logger = options.features["logging"].as(Halite::Logging) logger.writer.should be_a(SimpleLogger) end it "should became a file logger" do - Halite::Logger.register "simple", SimpleLogger + Halite::Logging.register "simple", SimpleLogger tempfile = Tempfile.new("halite_logger") options = Halite::Options.new.with_logger(format: "simple", file: tempfile.path, filemode: "w") - logger = options.features["logger"].as(Halite::Logger) + logger = options.features["logging"].as(Halite::Logging) logger.writer.should be_a(SimpleLogger) end @@ -287,23 +287,23 @@ describe Halite::Options do describe "#with_features" do it "should use a feature" do - options = Halite::Options.new.with_features("logger") - logger = options.features["logger"].as(Halite::Logger) - logger.writer.should be_a(Halite::Logger::Common) + options = Halite::Options.new.with_features("logging") + logger = options.features["logging"].as(Halite::Logging) + logger.writer.should be_a(Halite::Logging::Common) end it "should use a feature with options" do - options = Halite::Options.new.with_features("logger", logger: SimpleLogger.new) - logger = options.features["logger"].as(Halite::Logger) + options = Halite::Options.new.with_features("logging", logger: SimpleLogger.new) + logger = options.features["logging"].as(Halite::Logging) logger.writer.should be_a(SimpleLogger) end it "should use multiple features" do Halite.register_feature "simple", SimpleFeature - options = Halite::Options.new.with_features("logger", "simple") - logger = options.features["logger"].as(Halite::Logger) - logger.writer.should be_a(Halite::Logger::Common) + options = Halite::Options.new.with_features("logging", "simple") + logger = options.features["logging"].as(Halite::Logging) + logger.writer.should be_a(Halite::Logging::Common) simple = options.features["simple"].as(SimpleFeature) simple.should be_a(SimpleFeature) diff --git a/spec/halite_spec.cr b/spec/halite_spec.cr index a22a248b..c02af5e4 100644 --- a/spec/halite_spec.cr +++ b/spec/halite_spec.cr @@ -311,11 +311,11 @@ describe Halite do describe ".use" do describe "built-in features" do it "sets given feature name" do - client = Halite.use("logger") - client.options.features.has_key?("logger").should be_true - client.options.features["logger"].should be_a(Halite::Logger) - logger = client.options.features["logger"].as(Halite::Logger) - logger.writer.should be_a(Halite::Logger::Common) + client = Halite.use("logging") + client.options.features.has_key?("logging").should be_true + client.options.features["logging"].should be_a(Halite::Logging) + logger = client.options.features["logging"].as(Halite::Logging) + logger.writer.should be_a(Halite::Logging::Common) logger.writer.skip_request_body.should be_false logger.writer.skip_response_body.should be_false logger.writer.skip_benchmark.should be_false @@ -323,11 +323,11 @@ describe Halite do end it "sets given feature name and options" do - client = Halite.use("logger", logger: Halite::Logger::JSON.new(skip_request_body: true, colorize: false)) - client.options.features.has_key?("logger").should be_true - client.options.features["logger"].should be_a(Halite::Logger) - logger = client.options.features["logger"].as(Halite::Logger) - logger.writer.should be_a(Halite::Logger::JSON) + client = Halite.use("logging", logger: Halite::Logging::JSON.new(skip_request_body: true, colorize: false)) + client.options.features.has_key?("logging").should be_true + client.options.features["logging"].should be_a(Halite::Logging) + logger = client.options.features["logging"].as(Halite::Logging) + logger.writer.should be_a(Halite::Logging::JSON) logger.writer.skip_request_body.should be_true logger.writer.skip_response_body.should be_false logger.writer.skip_benchmark.should be_false diff --git a/src/halite/chainable.cr b/src/halite/chainable.cr index d3bec957..89f20a09 100644 --- a/src/halite/chainable.cr +++ b/src/halite/chainable.cr @@ -227,7 +227,7 @@ module Halite branch(default_options) end - # Returns `Options` self with given the logger which it integration from `Halite::Logger`. + # Returns `Options` self with given the logger which it integration from `Halite::Logging`. # # #### Simple logging # @@ -260,11 +260,11 @@ module Halite # # #### Use custom logger # - # Creating the custom logger by integration `Halite::Logger::Abstract` abstract class. + # Creating the custom logger by integration `Halite::Logging::Abstract` abstract class. # Here has two methods must be implement: `#request` and `#response`. # # ``` - # class CustomLogger < Halite::Logger::Abstract + # class CustomLogger < Halite::Logging::Abstract # def request(request) # @logger.info "| >> | %s | %s %s" % [request.verb, request.uri, request.body] # end @@ -275,7 +275,7 @@ module Halite # end # # # Add to adapter list (optional) - # Halite::Logger.register_adapter "custom", CustomLogger.new + # Halite::Logging.register_adapter "custom", CustomLogger.new # # Halite.logger(logger: CustomLogger.new) # .get("http://httpbin.org/get", params: {name: "foobar"}) @@ -287,7 +287,7 @@ module Halite # # => 2017-12-13 16:40:13 +08:00 | >> | GET | http://httpbin.org/get?name=foobar # # => 2017-12-13 16:40:15 +08:00 | << | 200 | http://httpbin.org/get?name=foobar application/json # ``` - def logger(logger = Halite::Logger::Common.new) + def logger(logger = Halite::Logging::Common.new) branch(default_options.with_logger(logger)) end @@ -337,7 +337,7 @@ module Halite # #### Use json logger # # ``` - # Halite.use("logger", format: "json") + # Halite.use("logging", format: "json") # .get("http://httpbin.org/get", params: {name: "foobar"}) # # # => { ... } @@ -345,7 +345,7 @@ module Halite # # #### Use common format logger and skip response body # ``` - # Halite.use("logger", format: "common", skip_response_body: true) + # Halite.use("logging", format: "common", skip_response_body: true) # .get("http://httpbin.org/get", params: {name: "foobar"}) # # # => 2018-08-28 14:58:26 +08:00 | request | GET | http://httpbin.org/get @@ -360,7 +360,7 @@ module Halite # Available features to review all subclasses of `Halite::Feature`. # # ``` - # Halite.use("logger", "your-custom-feature-name") + # Halite.use("logging", "your-custom-feature-name") # .get("http://httpbin.org/get", params: {name: "foobar"}) # ``` def use(*features) diff --git a/src/halite/features/logger.cr b/src/halite/features/logging.cr similarity index 81% rename from src/halite/features/logger.cr rename to src/halite/features/logging.cr index bf2ddaaf..525742ff 100644 --- a/src/halite/features/logger.cr +++ b/src/halite/features/logging.cr @@ -3,22 +3,22 @@ require "colorize" require "file_utils" module Halite - # Logger feature - class Logger < Feature - def self.new(format : String = "common", logger : Logger::Abstract? = nil, **opts) + # Logging feature + class Logging < Feature + def self.new(format : String = "common", logger : Logging::Abstract? = nil, **opts) return new(logger: logger) if logger - raise UnRegisterLoggerFormatError.new("Not avaiable logger format: #{format}") unless cls = Logger[format]? + raise UnRegisterLoggerFormatError.new("Not avaiable logging format: #{format}") unless cls = Logging[format]? logger = cls.new(**opts) new(logger: logger) end - DEFAULT_LOGGER = Logger::Common.new + DEFAULT_LOGGER = Logging::Common.new - getter writer : Logger::Abstract + getter writer : Logging::Abstract def initialize(**options) - @writer = options.fetch(:logger, DEFAULT_LOGGER).as(Logger::Abstract) + @writer = options.fetch(:logger, DEFAULT_LOGGER).as(Logging::Abstract) end def request(request) @@ -93,8 +93,8 @@ module Halite extend Register - Halite.register_feature "logger", self + Halite.register_feature "logging", self end end -require "./loggers/*" +require "./logging/*" diff --git a/src/halite/features/loggers/common.cr b/src/halite/features/logging/common.cr similarity index 97% rename from src/halite/features/loggers/common.cr rename to src/halite/features/logging/common.cr index fb3da864..2c0cb7ec 100644 --- a/src/halite/features/loggers/common.cr +++ b/src/halite/features/logging/common.cr @@ -1,7 +1,7 @@ require "file_utils" -class Halite::Logger - # Logger feature: Logger::Common +class Halite::Logging + # Logger feature: Logging::Common class Common < Abstract @request_time : Time? @@ -116,6 +116,6 @@ class Halite::Logger "#{digits.round(2).to_s}#{suffix}" end - Logger.register "common", self + Logging.register "common", self end end diff --git a/src/halite/features/loggers/json.cr b/src/halite/features/logging/json.cr similarity index 96% rename from src/halite/features/loggers/json.cr rename to src/halite/features/logging/json.cr index 15209a64..8b3fd508 100644 --- a/src/halite/features/loggers/json.cr +++ b/src/halite/features/logging/json.cr @@ -1,6 +1,6 @@ require "json" -class Halite::Logger +class Halite::Logging # Logger feature: Logger::JSON class JSON < Abstract @created_at : Time? = nil @@ -65,6 +65,6 @@ class Halite::Logger end end - Logger.register "json", self + Logging.register "json", self end end diff --git a/src/halite/options.cr b/src/halite/options.cr index a412d4f9..8a93f81f 100644 --- a/src/halite/options.cr +++ b/src/halite/options.cr @@ -187,14 +187,14 @@ module Halite # Returns `Logger` self with given format and the options of format. def with_logger(format : String, **opts) - raise UnRegisterLoggerFormatError.new("Not avaiable logger format: #{format}") unless format_cls = Logger[format]? + raise UnRegisterLoggerFormatError.new("Not avaiable logging format: #{format}") unless format_cls = Logging[format]? with_logger(format_cls.new(**opts)) end # Returns `Logger` self with given logger, depend on `with_features`. - def with_logger(logger : Halite::Logger::Abstract) + def with_logger(logger : Halite::Logging::Abstract) @logging = true - with_features("logger", logger: logger) + with_features("logging", logger: logger) self end @@ -239,15 +239,10 @@ module Halite # Quick enable logger # - # By defaults, use `Logger::Common` as logger output. + # By defaults, use `Logging::Common` as logger output. def logging=(logging : Bool) @logging = logging - - if logging - with_features("logger") - else - @features.delete("logger") - end + logging ? with_features("logging") : @features.delete("logging") end # Return if enable logging From 8f06c87c2a37d1388e84292b83cde1d916a15ce9 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 12:05:25 +0800 Subject: [PATCH 04/19] refactor(mime_type): Rename Halite::MimeTypes to Halite::MimeType, and merge Halite::MimeTypes.register_adapter/register_alias into Halite::MimeType.register --- spec/halite/mime_type_spec.cr | 28 +++++++--------------------- spec/halite/mime_types/json_spec.cr | 6 +++--- src/halite/mime_type.cr | 18 ++++++++---------- src/halite/mime_types/json.cr | 5 ++--- src/halite/response.cr | 4 ++-- 5 files changed, 22 insertions(+), 39 deletions(-) diff --git a/spec/halite/mime_type_spec.cr b/spec/halite/mime_type_spec.cr index 0080fa08..2f85be81 100644 --- a/spec/halite/mime_type_spec.cr +++ b/spec/halite/mime_type_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" require "yaml" -private class YAMLAdapter < Halite::MimeTypes::Adapter +private class YAMLAdapter < Halite::MimeType::Adapter def decode(string) YAML.parse string end @@ -11,28 +11,14 @@ private class YAMLAdapter < Halite::MimeTypes::Adapter end end -describe Halite::MimeTypes do +describe Halite::MimeType do it "should register an adapter" do - Halite::MimeTypes["yaml"]?.should be_nil - Halite::MimeTypes["yml"]?.should be_nil + Halite::MimeType["yaml"]?.should be_nil + Halite::MimeType["yml"]?.should be_nil - Halite::MimeTypes.register_adapter "application/x-yaml", YAMLAdapter.new - Halite::MimeTypes.register_alias "application/x-yaml", "yaml" - Halite::MimeTypes.register_alias "application/x-yaml", "yml" + Halite::MimeType.register YAMLAdapter.new, "application/x-yaml", "yaml", "yml" - Halite::MimeTypes["yaml"].should be_a YAMLAdapter - Halite::MimeTypes["yml"].should be_a YAMLAdapter - end - - it "should overwrite exists adapter" do - Halite::MimeTypes.register_adapter "application/json", YAMLAdapter.new - Halite::MimeTypes.register_alias "application/json", "json" - - Halite::MimeTypes["json"].should be_a YAMLAdapter - Halite::MimeTypes["json"].should_not be_a Halite::MimeTypes::JSON - - # Restore back for other specs - Halite::MimeTypes.register_adapter "application/json", Halite::MimeTypes::JSON.new - Halite::MimeTypes.register_alias "application/json", "json" + Halite::MimeType["yaml"].should be_a YAMLAdapter + Halite::MimeType["yml"].should be_a YAMLAdapter end end diff --git a/spec/halite/mime_types/json_spec.cr b/spec/halite/mime_types/json_spec.cr index c65a0df9..d2a7c7f7 100644 --- a/spec/halite/mime_types/json_spec.cr +++ b/spec/halite/mime_types/json_spec.cr @@ -3,17 +3,17 @@ require "../../spec_helper" private class Foo end -describe Halite::MimeTypes::JSON do +describe Halite::MimeType::JSON do describe "#encode" do it "shoulds works with to_json class" do - json = Halite::MimeTypes::JSON.new + json = Halite::MimeType::JSON.new json.encode({name: "foo"}).should eq(%Q{{"name":"foo"}}) end end describe "#decode" do it "shoulds works with json string" do - json = Halite::MimeTypes::JSON.new + json = Halite::MimeType::JSON.new json.decode(%Q{{"name": "foo"}}).should be_a(JSON::Any) json.decode(%Q{{"name": "foo"}}).should eq({"name" => "foo"}) end diff --git a/src/halite/mime_type.cr b/src/halite/mime_type.cr index 332443c2..d8acf378 100644 --- a/src/halite/mime_type.cr +++ b/src/halite/mime_type.cr @@ -1,15 +1,14 @@ module Halite - module MimeTypes - @@adapters = {} of String => Adapter - - def self.register_adapter(name : String, adapter : Adapter) - @@adapters[name] = adapter - end - + module MimeType + @@adapters = {} of String => MimeType::Adapter @@aliases = {} of String => String - def self.register_alias(name : String, shortcut : String) - @@aliases[shortcut] = name + def self.register(adapter : MimeType::Adapter, name : String, *shortcuts) + @@adapters[name] = adapter + shortcuts.each do |shortcut| + next unless shortcut.is_a?(String) + @@aliases[shortcut] = name + end unless shortcuts.empty? end def self.[](name : String) @@ -23,7 +22,6 @@ module Halite private def self.normalize(name : String) @@aliases.fetch name, name end - abstract class Adapter abstract def encode(obj) abstract def decode(string) diff --git a/src/halite/mime_types/json.cr b/src/halite/mime_types/json.cr index 4a10083b..65191909 100644 --- a/src/halite/mime_types/json.cr +++ b/src/halite/mime_types/json.cr @@ -1,6 +1,6 @@ require "json" -module Halite::MimeTypes +module Halite::MimeType class JSON < Adapter def encode(obj) obj.to_json @@ -12,5 +12,4 @@ module Halite::MimeTypes end end -Halite::MimeTypes.register_adapter "application/json", Halite::MimeTypes::JSON.new -Halite::MimeTypes.register_alias "application/json", "json" +Halite::MimeType.register Halite::MimeType::JSON.new, "application/json", "json" diff --git a/src/halite/response.cr b/src/halite/response.cr index a1a3feec..4a2f2e15 100644 --- a/src/halite/response.cr +++ b/src/halite/response.cr @@ -79,9 +79,9 @@ module Halite name ||= content_type raise Halite::Error.new("No match MIME type: #{name}") unless name - raise Halite::UnRegisterMimeTypeError.new("unregister MIME type adapter: #{name}") unless MimeTypes[name]? + raise Halite::UnRegisterMimeTypeError.new("unregister MIME type adapter: #{name}") unless MimeType[name]? - MimeTypes[name].decode to_s + MimeType[name].decode to_s end # Return raw of response From c2d997d48bc853640d47b8379400a8085a1053b7 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 12:05:25 +0800 Subject: [PATCH 05/19] refactor(mime_type): Rename Halite::MimeTypes to Halite::MimeType, and merge Halite::MimeTypes.register_adapter/register_alias into Halite::MimeType.register --- spec/halite/features/{logger_spec.cr => logging_spec.cr} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/halite/features/{logger_spec.cr => logging_spec.cr} (100%) diff --git a/spec/halite/features/logger_spec.cr b/spec/halite/features/logging_spec.cr similarity index 100% rename from spec/halite/features/logger_spec.cr rename to spec/halite/features/logging_spec.cr From f62437e54c2cd03b20f019bd7c39a07687eccd84 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 16:30:55 +0800 Subject: [PATCH 06/19] fix(logging): Remove instance Logging with format --- spec/halite/ext/namedtuple_to_h.cr | 16 ++++++++ spec/halite/features/logging_spec.cr | 12 ------ spec/halite_spec.cr | 61 +++++++++++++++++++++++++++- spec/spec_helper.cr | 12 ++++++ src/halite/chainable.cr | 5 +-- src/halite/features/logging.cr | 14 ++----- src/halite/mime_type.cr | 1 + 7 files changed, 94 insertions(+), 27 deletions(-) create mode 100644 spec/halite/ext/namedtuple_to_h.cr diff --git a/spec/halite/ext/namedtuple_to_h.cr b/spec/halite/ext/namedtuple_to_h.cr new file mode 100644 index 00000000..d5b5bc6c --- /dev/null +++ b/spec/halite/ext/namedtuple_to_h.cr @@ -0,0 +1,16 @@ +struct NamedTuple + # Returns a `Hash` with the keys and values in this named tuple. + # + # TODO: This is fix bug, It will remove if PR is merged https://github.com/crystal-lang/crystal/pull/6628 + def to_h + raise "Can't convert an empty NamedTuple to a Hash" if empty? + + {% if T.size > 0 %} + { + {% for key in T %} + {{key.symbolize}} => self[{{key.symbolize}}], + {% end %} + } + {% end %} + end +end diff --git a/spec/halite/features/logging_spec.cr b/spec/halite/features/logging_spec.cr index 28fe5525..40b73b7e 100644 --- a/spec/halite/features/logging_spec.cr +++ b/spec/halite/features/logging_spec.cr @@ -1,17 +1,5 @@ require "../../spec_helper" -private class SimpleLogger < Halite::Logging::Abstract - def request(request) - @logger.info "request" - end - - def response(response) - @logger.info "response" - end - - Halite::Logging.register "simple", self -end - describe Halite::Logging do it "should register an format" do Halite::Logging["simple"].should eq(SimpleLogger) diff --git a/spec/halite_spec.cr b/spec/halite_spec.cr index c02af5e4..855fbe70 100644 --- a/spec/halite_spec.cr +++ b/spec/halite_spec.cr @@ -1,4 +1,5 @@ require "./spec_helper" +require "tempfile" describe Halite do describe ".new" do @@ -308,6 +309,64 @@ describe Halite do end end + describe ".logger" do + it "should use logging" do + client = Halite.logger + client.options.features.has_key?("logging").should be_true + client.options.features["logging"].should be_a(Halite::Logging) + logger = client.options.features["logging"].as(Halite::Logging) + logger.writer.should be_a(Halite::Logging::Common) + logger.writer.skip_request_body.should be_false + logger.writer.skip_response_body.should be_false + logger.writer.skip_benchmark.should be_false + logger.writer.colorize.should be_true + end + + it "sets logging with format" do + client = Halite.logger(format: "json", skip_response_body: true) + client.options.features.has_key?("logging").should be_true + client.options.features["logging"].should be_a(Halite::Logging) + logger = client.options.features["logging"].as(Halite::Logging) + logger.writer.should be_a(Halite::Logging::JSON) + logger.writer.skip_request_body.should be_false + logger.writer.skip_response_body.should be_true + logger.writer.skip_benchmark.should be_false + logger.writer.colorize.should be_true + end + + it "sets logging into file" do + tempfile = Tempfile.new("halite-spec-logging") + + client = Halite.logger(file: tempfile.path, skip_response_body: true) + client.options.features.has_key?("logging").should be_true + client.options.features["logging"].should be_a(Halite::Logging) + logger = client.options.features["logging"].as(Halite::Logging) + logger.writer.should be_a(Halite::Logging::Common) + logger.writer.skip_request_body.should be_false + logger.writer.skip_response_body.should be_true + logger.writer.skip_benchmark.should be_false + logger.writer.colorize.should be_true + + response = client.get SERVER.endpoint + logs = File.read_lines(tempfile.path).join("\n") + logs.should contain("request") + logs.should contain("response") + logs.should_not contain("Mock Server is running.") + end + + it "sets logging with custom logger" do + client = Halite.logger(logger: SimpleLogger.new(skip_benchmark: true)) + client.options.features.has_key?("logging").should be_true + client.options.features["logging"].should be_a(Halite::Logging) + logger = client.options.features["logging"].as(Halite::Logging) + logger.writer.should be_a(SimpleLogger) + logger.writer.skip_request_body.should be_false + logger.writer.skip_response_body.should be_false + logger.writer.skip_benchmark.should be_true + logger.writer.colorize.should be_true + end + end + describe ".use" do describe "built-in features" do it "sets given feature name" do @@ -322,7 +381,7 @@ describe Halite do logger.writer.colorize.should be_true end - it "sets given feature name and options" do + it "sets logging with logger" do client = Halite.use("logging", logger: Halite::Logging::JSON.new(skip_request_body: true, colorize: false)) client.options.features.has_key?("logging").should be_true client.options.features["logging"].should be_a(Halite::Logging) diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 51cb5863..5c17be70 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -54,6 +54,18 @@ module TestInterceptors end end +class SimpleLogger < Halite::Logging::Abstract + def request(request) + @logger.info "request" + end + + def response(response) + @logger.info "response" + end + + Halite::Logging.register "simple", self +end + #################### # Start mock server #################### diff --git a/src/halite/chainable.cr b/src/halite/chainable.cr index 89f20a09..55ed2962 100644 --- a/src/halite/chainable.cr +++ b/src/halite/chainable.cr @@ -287,7 +287,7 @@ module Halite # # => 2017-12-13 16:40:13 +08:00 | >> | GET | http://httpbin.org/get?name=foobar # # => 2017-12-13 16:40:15 +08:00 | << | 200 | http://httpbin.org/get?name=foobar application/json # ``` - def logger(logger = Halite::Logging::Common.new) + def logger(logger : Halite::Logging::Abstract = Halite::Logging::Common.new) branch(default_options.with_logger(logger)) end @@ -319,7 +319,6 @@ module Halite skip_request_body = false, skip_response_body = false, skip_benchmark = false, colorize = true) opts = { - format: format, file: file, filemode: filemode, skip_request_body: skip_request_body, @@ -327,7 +326,7 @@ module Halite skip_benchmark: skip_benchmark, colorize: colorize, } - branch(default_options.with_logger(**opts)) + branch(default_options.with_logger(format, **opts)) end # Turn on given features and its options. diff --git a/src/halite/features/logging.cr b/src/halite/features/logging.cr index 525742ff..3f2cabaf 100644 --- a/src/halite/features/logging.cr +++ b/src/halite/features/logging.cr @@ -5,20 +5,12 @@ require "file_utils" module Halite # Logging feature class Logging < Feature - def self.new(format : String = "common", logger : Logging::Abstract? = nil, **opts) - return new(logger: logger) if logger - raise UnRegisterLoggerFormatError.new("Not avaiable logging format: #{format}") unless cls = Logging[format]? - - logger = cls.new(**opts) - new(logger: logger) - end - DEFAULT_LOGGER = Logging::Common.new getter writer : Logging::Abstract def initialize(**options) - @writer = options.fetch(:logger, DEFAULT_LOGGER).as(Logging::Abstract) + @writer = (logger = options[:logger]?) ? logger.as(Logging::Abstract) : DEFAULT_LOGGER end def request(request) @@ -55,7 +47,7 @@ module Halite def initialize(@skip_request_body = false, @skip_response_body = false, @skip_benchmark = false, @colorize = true, @io : IO = STDOUT) - @logger = ::Logger.new(@io, ::Logger::DEBUG, default_formatter, "halite") + @logger = Logger.new(@io, ::Logger::DEBUG, default_formatter, "halite") Colorize.enabled = @colorize end @@ -65,7 +57,7 @@ module Halite abstract def response(response) def default_formatter - ::Logger::Formatter.new do |_, datetime, _, message, io| + Logger::Formatter.new do |_, datetime, _, message, io| io << datetime.to_s << " " << message end end diff --git a/src/halite/mime_type.cr b/src/halite/mime_type.cr index d8acf378..ca871e6f 100644 --- a/src/halite/mime_type.cr +++ b/src/halite/mime_type.cr @@ -22,6 +22,7 @@ module Halite private def self.normalize(name : String) @@aliases.fetch name, name end + abstract class Adapter abstract def encode(obj) abstract def decode(string) From 44c8f4421a49a71426c57a7697b1ac50a4467779 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 17:06:17 +0800 Subject: [PATCH 07/19] feat(logging): apply skip_request_body, skip_response_body, skip_benchmark to json format --- spec/halite/features/logging_spec.cr | 2 +- src/halite/features/logging.cr | 24 ++++++++++- src/halite/features/logging/common.cr | 40 +++++++----------- src/halite/features/logging/json.cr | 58 ++++++++++++++++++++++----- 4 files changed, 87 insertions(+), 37 deletions(-) diff --git a/spec/halite/features/logging_spec.cr b/spec/halite/features/logging_spec.cr index 40b73b7e..b5786a4f 100644 --- a/spec/halite/features/logging_spec.cr +++ b/spec/halite/features/logging_spec.cr @@ -1,7 +1,7 @@ require "../../spec_helper" describe Halite::Logging do - it "should register an format" do + it "should register a format" do Halite::Logging["simple"].should eq(SimpleLogger) Halite::Logging.availables.should eq ["common", "json", "simple"] end diff --git a/src/halite/features/logging.cr b/src/halite/features/logging.cr index 3f2cabaf..5b2ec043 100644 --- a/src/halite/features/logging.cr +++ b/src/halite/features/logging.cr @@ -45,6 +45,8 @@ module Halite getter skip_benchmark : Bool getter colorize : Bool + @request_time : Time? + def initialize(@skip_request_body = false, @skip_response_body = false, @skip_benchmark = false, @colorize = true, @io : IO = STDOUT) @logger = Logger.new(@io, ::Logger::DEBUG, default_formatter, "halite") @@ -56,11 +58,31 @@ module Halite abstract def request(request) abstract def response(response) - def default_formatter + protected def default_formatter Logger::Formatter.new do |_, datetime, _, message, io| io << datetime.to_s << " " << message end end + + protected def human_time(elapsed : Time::Span) + elapsed = elapsed.to_f + case Math.log10(elapsed) + when 0..Float64::MAX + digits = elapsed + suffix = "s" + when -3..0 + digits = elapsed * 1000 + suffix = "ms" + when -6..-3 + digits = elapsed * 1_000_000 + suffix = "µs" + else + digits = elapsed * 1_000_000_000 + suffix = "ns" + end + + "#{digits.round(2).to_s}#{suffix}" + end end @@formats = {} of String => Abstract.class diff --git a/src/halite/features/logging/common.cr b/src/halite/features/logging/common.cr index 2c0cb7ec..9036bba2 100644 --- a/src/halite/features/logging/common.cr +++ b/src/halite/features/logging/common.cr @@ -1,10 +1,20 @@ -require "file_utils" - class Halite::Logging - # Logger feature: Logging::Common + # Common logger format + # + # Instance variables to check `Halite::Logging::Abstract` + # + # ``` + # Halite.use("logging", logger: Halite::Logging::Common.new(skip_request_body: true)) + # .get("http://httpbin.org/get") + # + # # Or + # Halite.logger(format: "common", skip_request_body: true) + # .get("http://httpbin.org/get") + # + # # => 2018-08-31 16:56:12 +08:00 | request | GET | http://httpbin.org/get + # # => 2018-08-31 16:56:13 +08:00 | response | 200 | http://httpbin.org/get | 1.08s | application/json + # ``` class Common < Abstract - @request_time : Time? - def request(request) message = String.build do |io| io << "| request |" << colorful_method(request.verb) @@ -96,26 +106,6 @@ class Halite::Logging false end - private def human_time(elapsed : Time::Span) - elapsed = elapsed.to_f - case Math.log10(elapsed) - when 0..Float64::MAX - digits = elapsed - suffix = "s" - when -3..0 - digits = elapsed * 1000 - suffix = "ms" - when -6..-3 - digits = elapsed * 1_000_000 - suffix = "µs" - else - digits = elapsed * 1_000_000_000 - suffix = "ns" - end - - "#{digits.round(2).to_s}#{suffix}" - end - Logging.register "common", self end end diff --git a/src/halite/features/logging/json.cr b/src/halite/features/logging/json.cr index 8b3fd508..d6934018 100644 --- a/src/halite/features/logging/json.cr +++ b/src/halite/features/logging/json.cr @@ -1,19 +1,51 @@ require "json" class Halite::Logging - # Logger feature: Logger::JSON + # JSON logger format + # + # Instance variables to check `Halite::Logging::Abstract`. + # + # In JSON format, if you set skip some key, it will return `false`. + # + # ``` + # Halite.use("logging", logger: Halite::Logging::JSON.new(skip_request_body: true)) + # .get("http://httpbin.org/get") + # + # # Or + # Halite.logger(format: "json", skip_request_body: true) + # .get("http://httpbin.org/get") + # + # # => { + # # => "created_at": "2018-08-31T16:53:57+08:00:00", + # # => "entry": { + # # => "request": { + # # => "body": "", + # # => "headers": { ... }, + # # => "method": "GET", + # # => "url": "http://httpbin.org/anything", + # # => "timestamp": "2018-08-31T16:53:59+08:00:00" + # # => }, + # # => "response": { + # # => "body": false, + # # => "header": { ... }, + # # => "status_code": 200, + # # => "http_version": "HTTP/1.1", + # # => "timestamp": "2018-08-31T16:53:59+08:00:00" + # # => } + # # => } + # # => } + # ``` class JSON < Abstract - @created_at : Time? = nil @request : Request? = nil @response : Response? = nil def request(request) - @created_at = Time.now - @request = Request.new(request) + @request_time = Time.now + @request = Request.new(request, @skip_request_body) end def response(response) - @response = Response.new(response) + @response = Response.new(response, @skip_response_body) @logger.info raw end @@ -24,8 +56,14 @@ class Halite::Logging end private def raw + elapsed : String? = nil + if !@skip_benchmark && (request_time = @request_time) + elapsed = human_time(Time.now - request_time) + end + { - "created_at" => Time::Format::RFC_3339.format(@created_at.not_nil!, 0), + "created_at" => Time::Format::RFC_3339.format(@request_time.not_nil!, 0), + "elapsed" => elapsed, "entry" => { "request" => @request.not_nil!.to_h, "response" => @response.not_nil!.to_h, @@ -35,12 +73,12 @@ class Halite::Logging # :nodoc: private struct Request - def initialize(@request : Halite::Request) + def initialize(@request : Halite::Request, @skip_body = false) end def to_h { - "body" => @request.body, + "body" => @skip_body ? false : @request.body, "headers" => @request.headers.to_h, "method" => @request.verb, "url" => @request.uri.to_s, @@ -51,12 +89,12 @@ class Halite::Logging # :nodoc: private struct Response - def initialize(@response : Halite::Response) + def initialize(@response : Halite::Response, @skip_body = false) end def to_h { - "body" => @response.body, + "body" => @skip_body ? false : @response.body, "header" => @response.headers.to_h, "status_code" => @response.status_code, "http_version" => @response.version, From 478f087363739c5c55970dd37f6d569e1f5b29fc Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 17:16:06 +0800 Subject: [PATCH 08/19] test: fix ci --- spec/halite/features/logging_spec.cr | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/spec/halite/features/logging_spec.cr b/spec/halite/features/logging_spec.cr index b5786a4f..3183b6a7 100644 --- a/spec/halite/features/logging_spec.cr +++ b/spec/halite/features/logging_spec.cr @@ -1,9 +1,18 @@ require "../../spec_helper" +private class NulleLogger < Halite::Logging::Abstract + def request(request) + end + + def response(response) + end +end + describe Halite::Logging do it "should register a format" do - Halite::Logging["simple"].should eq(SimpleLogger) - Halite::Logging.availables.should eq ["common", "json", "simple"] + Halite::Logging.register "null", NulleLogger + Halite::Logging.availables.includes?("null").should be_true + Halite::Logging["null"].should eq(NulleLogger) end it "should use common as default logger" do @@ -16,8 +25,8 @@ describe Halite::Logging do end it "should use custom logger" do - logger = Halite::Logging.new(logger: SimpleLogger.new) - logger.writer.should be_a(SimpleLogger) + logger = Halite::Logging.new(logger: NulleLogger.new) + logger.writer.should be_a(NulleLogger) logger.writer.skip_request_body.should be_false logger.writer.skip_response_body.should be_false logger.writer.skip_benchmark.should be_false From 72880e18d97513e4892408b11020504038461b38 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 17:42:09 +0800 Subject: [PATCH 09/19] doc: update new changes in README --- README.md | 55 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 87153d86..ae5b643a 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ [![Tag](https://img.shields.io/github/tag/icyleaf/halite.svg)](https://github.com/icyleaf/halite/blob/master/CHANGELOG.md) [![Build Status](https://img.shields.io/circleci/project/github/icyleaf/halite/master.svg?style=flat)](https://circleci.com/gh/icyleaf/halite) -Crystal HTTP Requests with a chainable REST API, built-in sessions and loggers written by [Crystal](https://crystal-lang.org/). +HTTP Requests with a chainable REST API, built-in sessions and loggers written by [Crystal](https://crystal-lang.org/). Inspired from the **awesome** Ruby's [HTTP](https://github.com/httprb/http)/[RESTClient](https://github.com/rest-client/rest-client) gem and Python's [requests](https://github.com/requests/requests). -Build in crystal version >= `v0.25.0`, this document valid in latest commit. +Build in crystal version >= `v0.25.0`, this document valid with latest commit. ## Index @@ -41,6 +41,7 @@ Build in crystal version >= `v0.25.0`, this document valid in latest commit. - [Error Handling](#error-handling) - [Raise for status code](#raise-for-status-code) - [Advanced Usage](#advanced-usage) + - [Configuring](#configuring) - [Sessions](#sessions) - [Logging](#logging) - [JSON-formatted logging](#json-formatted-logging) @@ -326,6 +327,17 @@ r.history # ] ``` +**NOTE**: It contains the `Response` object if you use `history` and HTTP was not a `30x`, For example: + +```crystal +r = Halite.get("http://httpbin.org/get") +r.history.size # => 0 + +r = Halite.follow + .get("http://httpbin.org/get") +r.history.size # => 1 +``` + #### Timeout By default, the Halite does not enforce timeout on a request. @@ -412,7 +424,7 @@ we can inherit `Halite::MimeTypes::Adapter` make our adapter: ```crystal # Define a MIME type adapter -class YAMLAdapter < Halite::MimeTypes::Adapter +class YAMLAdapter < Halite::MimeType::Adapter def decode(string) YAML.parse(string) end @@ -423,9 +435,7 @@ class YAMLAdapter < Halite::MimeTypes::Adapter end # Register to Halite to invoke -Halite::MimeTypes.register_adapter "application/x-yaml", YAMLAdapter.new -Halite::MimeTypes.register_alias "application/x-yaml", "yaml" -Halite::MimeTypes.register_alias "application/x-yaml", "yml" +Halite::MimeType.register YAMLAdapter.new, "application/x-yaml", "yaml", "yml" # Test it! r = Halite.get "https://raw.githubusercontent.com/icyleaf/halite/master/shard.yml" @@ -485,26 +495,39 @@ end ## Advanced Usage -### Sessions - -As like [requests.Session()](http://docs.python-requests.org/en/master/user/advanced/#session-objects), Halite built-in session by default. +### Configuring -Let's persist some cookies across requests: +Halite provides a traditional way to instance client, and you can configure any chainable methods with block: ```crystal -client = Halite::Client.new -# Or configure it client = Halite::Client.new do # Set basic auth - basic_auth "name", "foo" + basic_auth "username", "password" # Enable logging logging true - # Set read timeout to one minute - timeout(read: 1.minutes) + # Set timeout + timeout 10.seconds + + # Set user agent + headers user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36" end +# You also can configure in this way +client.accept("application/json") + +r = client.get("http://httpbin.org/get") +``` + +### Sessions + +As like [requests.Session()](http://docs.python-requests.org/en/master/user/advanced/#session-objects), Halite built-in session by default. + +Let's persist some cookies across requests: + +```crystal +client = Halite::Client.new client.get("http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0") client.get("http://httpbin.org/cookies") # => 2018-06-25 18:41:05 +08:00 | request | GET | http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0 @@ -623,7 +646,7 @@ in your HTTP client and allowing you to monitor outgoing requests, and incoming Avaiabled features: -- logging (Cool, aha!) +- logging (Yes, logging is based on feature, cool, aha!) #### Write a simple feature From 655ad4d180a0e46e69e8dd2a6f46397ff0ef953f Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 17:51:06 +0800 Subject: [PATCH 10/19] chore: clean up --- spec/halite_spec.cr | 2 +- src/halite/features/logging.cr | 3 ++- src/halite/features/logging/json.cr | 42 ++++++++++++++++------------- src/halite/options.cr | 2 +- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/spec/halite_spec.cr b/spec/halite_spec.cr index 855fbe70..42317e76 100644 --- a/spec/halite_spec.cr +++ b/spec/halite_spec.cr @@ -347,7 +347,7 @@ describe Halite do logger.writer.skip_benchmark.should be_false logger.writer.colorize.should be_true - response = client.get SERVER.endpoint + client.get SERVER.endpoint logs = File.read_lines(tempfile.path).join("\n") logs.should contain("request") logs.should contain("response") diff --git a/src/halite/features/logging.cr b/src/halite/features/logging.cr index 5b2ec043..ec6216e9 100644 --- a/src/halite/features/logging.cr +++ b/src/halite/features/logging.cr @@ -23,7 +23,7 @@ module Halite response end - # Logger Abstract + # Logging format Abstract abstract class Abstract def self.new(file : String? = nil, filemode = "a", skip_request_body = false, skip_response_body = false, @@ -87,6 +87,7 @@ module Halite @@formats = {} of String => Abstract.class + # Logging format register module Register def register(name : String, format : Abstract.class) @@formats[name] = format diff --git a/src/halite/features/logging/json.cr b/src/halite/features/logging/json.cr index d6934018..5f2139d2 100644 --- a/src/halite/features/logging/json.cr +++ b/src/halite/features/logging/json.cr @@ -14,26 +14,30 @@ class Halite::Logging # # Or # Halite.logger(format: "json", skip_request_body: true) # .get("http://httpbin.org/get") + # ``` + # + # Log will look like: # - # # => { - # # => "created_at": "2018-08-31T16:53:57+08:00:00", - # # => "entry": { - # # => "request": { - # # => "body": "", - # # => "headers": { ... }, - # # => "method": "GET", - # # => "url": "http://httpbin.org/anything", - # # => "timestamp": "2018-08-31T16:53:59+08:00:00" - # # => }, - # # => "response": { - # # => "body": false, - # # => "header": { ... }, - # # => "status_code": 200, - # # => "http_version": "HTTP/1.1", - # # => "timestamp": "2018-08-31T16:53:59+08:00:00" - # # => } - # # => } - # # => } + # ``` + # { + # "created_at": "2018-08-31T16:53:57+08:00:00", + # "entry": { + # "request": { + # "body": "", + # "headers": { ... }, + # "method": "GET", + # "url": "http://httpbin.org/anything", + # "timestamp": "2018-08-31T16:53:59+08:00:00" + # }, + # "response": { + # "body": false, + # "header": { ... }, + # "status_code": 200, + # "http_version": "HTTP/1.1", + # "timestamp": "2018-08-31T16:53:59+08:00:00" + # } + # } + # } # ``` class JSON < Abstract @request : Request? = nil diff --git a/src/halite/options.cr b/src/halite/options.cr index 8a93f81f..3cae74f0 100644 --- a/src/halite/options.cr +++ b/src/halite/options.cr @@ -254,7 +254,7 @@ module Halite def merge(options : Halite::Options) : Halite::Options if options.headers != default_headers # Remove default key to make sure it is not to overwrite new one. - default_headers.each do |key, value| + default_headers.each do |key, _| options.headers.delete(key) if options.headers[key] = default_headers[key] end From 8842c4849bb791e93e4f512b6a41c5c80e28dba8 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 17:57:18 +0800 Subject: [PATCH 11/19] fix(cache): keep with new feature spec --- spec/halite/features/cache_spec.cr | 8 ++++++++ src/halite/features/cache.cr | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 spec/halite/features/cache_spec.cr diff --git a/spec/halite/features/cache_spec.cr b/spec/halite/features/cache_spec.cr new file mode 100644 index 00000000..a602f032 --- /dev/null +++ b/spec/halite/features/cache_spec.cr @@ -0,0 +1,8 @@ +require "../../spec_helper" + +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 +end diff --git a/src/halite/features/cache.cr b/src/halite/features/cache.cr index 9bd0c07f..dc755408 100644 --- a/src/halite/features/cache.cr +++ b/src/halite/features/cache.cr @@ -2,7 +2,7 @@ require "json" require "digest" require "file_utils" -module Halite::Features +module Halite # Cache feature use for caching HTTP response to local storage to speed up in developing stage. # # It has the following options: @@ -44,7 +44,7 @@ module Halite::Features chain.return(response) end - private def cache(chain : Interceptor::Chain, &block : -> Response) + private def cache(chain, &block : -> Response) if response = find_cache(chain.request) return response end @@ -104,6 +104,6 @@ module Halite::Features end end - Halite::Features.register "cache", self + Halite.register_feature "cache", self end end From 7f999538073b6e02e59dc53951073ca90d3e5c72 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 18:42:02 +0800 Subject: [PATCH 12/19] feat(cache): store cache into a directory with metdata file and body file --- src/halite/features/cache.cr | 76 +++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/src/halite/features/cache.cr b/src/halite/features/cache.cr index dc755408..75789396 100644 --- a/src/halite/features/cache.cr +++ b/src/halite/features/cache.cr @@ -19,9 +19,9 @@ module Halite # - `X-Cached-By`: Always return "Halite" # # ``` - # Halite.use("cache").get "http://httpbin.org/anything" # request a HTTP - # r = Halite.use("cache").get "http://httpbin.org/anything" # request from local storage - # r.headers # => {..., "X-Cached-At" => "2018-08-30 10:41:14 UTC", "X-Cached-By" => "Halite", "X-Cached-Expires-At" => "2018-08-30 10:41:19 UTC", "X-Cached-Key" => "2bb155e6c8c47627da3d91834eb4249a"}} + # Halite.use("cache").get "http://httpbin.org/anything" # request a HTTP + # r = Halite.use("cache").get "http://httpbin.org/anything" # request from local storage + # r.headers # => {..., "X-Cached-At" => "2018-08-30 10:41:14 UTC", "X-Cached-By" => "Halite", "X-Cached-Expires-At" => "2018-08-30 10:41:19 UTC", "X-Cached-Key" => "2bb155e6c8c47627da3d91834eb4249a"}} # ``` class Cache < Feature DEFAULT_PATH = "cache/" @@ -31,9 +31,16 @@ module Halite @debug : Bool def initialize(**options) - @path = options.fetch(:path, DEFAULT_PATH).as(String) - @expires = options[:expires]?.as(Time::Span?) @debug = options.fetch(:debug, true).as(Bool) + @path = options.fetch(:path, DEFAULT_PATH).as(String) + @expires = case expires = options[:expires]? + when Time::Span + expires.as(Time::Span) + when Int32 + Time::Span.new(seconds: expires.as(Int32), nanoseconds: 0) + else + raise "Only accept Int32 and Time::Span type." + end end def intercept(chain) @@ -56,27 +63,38 @@ module Halite private def find_cache(request : Request) : Response? key = generate_cache_key(request) - file = File.join(@path, key) + path = File.join(@path, key) + file = File.join(path, "#{key}.cache") if File.exists?(file) && !cache_expired?(file) - cache = JSON.parse(File.open(file)).as_h - status_code = cache["status_code"].as_i - body = cache["body"].as_s - headers = cache["headers"].as_h.each_with_object(HTTP::Headers.new) do |(key, value), obj| - obj[key] = value.as_s - end - - if @debug - headers["X-Cached-Key"] = key - headers["X-Cached-At"] = cache_created_time(file).to_s - headers["X-Cached-Expires-At"] = @expires ? (cache_created_time(file) + @expires.not_nil!).to_s : "None" - headers["X-Cached-By"] = "Halite" + status_code = 200 + headers = HTTP::Headers.new + if metadata = find_metadata(path) + status_code = metadata["status_code"].as_i + metadata["headers"].as_h.each do |key, value| + headers[key] = value.as_s + end + + if @debug + headers["X-Cached-Key"] = key + headers["X-Cached-At"] = cache_created_time(file).to_s + headers["X-Cached-Expires-At"] = @expires ? (cache_created_time(file) + @expires.not_nil!).to_s : "None" + headers["X-Cached-By"] = "Halite" + end end + body = File.read_lines(file).join("\n") return Response.new(request.uri, status_code, body, headers) end end + private def find_metadata(path) + file = File.join(path, "metadata.json") + if File.exists?(file) + JSON.parse(File.open(file)).as_h + end + end + private def cache_expired?(file) return false unless expires = @expires file_modified_time = cache_created_time(file) @@ -92,18 +110,30 @@ module Halite end private def write_cache(request, response) - FileUtils.mkdir_p(@path) unless Dir.exists?(@path) - key = generate_cache_key(request) - File.open(File.join(@path, key), "w") do |f| + path = File.join(@path, key) + + FileUtils.mkdir_p(path) unless Dir.exists?(path) + + write_metadata(path, response) + write_body(path, key, response) + end + + private def write_metadata(path, response) + File.open(File.join(path, "metadata.json"), "w") do |f| f.puts({ "status_code" => response.status_code, - "headers" => response.headers.to_h, - "body" => response.body + "headers" => response.headers.to_h, }.to_json) end end + private def write_body(path, key, response) + File.open(File.join(path, "#{key}.cache"), "w") do |f| + f.puts response.body + end + end + Halite.register_feature "cache", self end end From 195d5acb09ad04fab8778bd97b8252ff6f088cc6 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 18:46:12 +0800 Subject: [PATCH 13/19] doc: update cache comment --- src/halite/features/cache.cr | 7 +++++++ src/halite/features/logging.cr | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/halite/features/cache.cr b/src/halite/features/cache.cr index 75789396..a6adca54 100644 --- a/src/halite/features/cache.cr +++ b/src/halite/features/cache.cr @@ -30,6 +30,13 @@ module Halite @expires : Time::Span? @debug : Bool + # return a new Cache instance + # + # Accepts argument: + # + # - **debug**: `Bool` + # - **path**: `String` + # - **expires**: `(Int32 | Time::Span)?` def initialize(**options) @debug = options.fetch(:debug, true).as(Bool) @path = options.fetch(:path, DEFAULT_PATH).as(String) diff --git a/src/halite/features/logging.cr b/src/halite/features/logging.cr index ec6216e9..c4be2146 100644 --- a/src/halite/features/logging.cr +++ b/src/halite/features/logging.cr @@ -9,6 +9,11 @@ module Halite getter writer : Logging::Abstract + # return a new Cache instance + # + # Accepts argument: + # + # - **logger**: `Logging::Abstract` def initialize(**options) @writer = (logger = options[:logger]?) ? logger.as(Logging::Abstract) : DEFAULT_LOGGER end From 990b3bd3558b26872a23e222331bb27ffaad0eb0 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Fri, 31 Aug 2018 19:17:05 +0800 Subject: [PATCH 14/19] test: add cache feature spec --- spec/halite/features/cache_spec.cr | 39 ++++++++++++++++++++++++++++++ src/halite/features/cache.cr | 9 ++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/spec/halite/features/cache_spec.cr b/spec/halite/features/cache_spec.cr index a602f032..aab16577 100644 --- a/spec/halite/features/cache_spec.cr +++ b/spec/halite/features/cache_spec.cr @@ -5,4 +5,43 @@ describe Halite::Cache do Halite.has_feature?("cache").should be_true Halite.feature("cache").should eq(Halite::Cache) 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=halite#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}) + chain = Halite::Feature::Chain.new(request, nil, Halite::Options.new) do + response + end + + feature = Halite::Cache.new + feature.path.should eq(Halite::Cache::DEFAULT_PATH) + feature.expires.should be_nil + feature.debug.should be_true + + feature.intercept(chain) + + cache_key = Digest::MD5.hexdigest("#{request.verb}-#{request.uri}-#{request.body}") + cache_path = File.join(feature.path, cache_key) + + metadata_file = File.join(cache_path, "metadata.json") + body_file = File.join(cache_path, "#{cache_key}.cache") + + Dir.exists?(cache_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 + metadata["status_code"].should eq(200) + metadata["headers"].as_h["Content-Type"].should eq("application/json") + metadata["headers"].as_h["Content-Length"].should eq(body.size.to_s) + + cache_body = File.read_lines(body_file).join("\n") + cache_body.should eq(body) + + # Clean up + FileUtils.rm_rf feature.path + end + end end diff --git a/src/halite/features/cache.cr b/src/halite/features/cache.cr index a6adca54..901f58b7 100644 --- a/src/halite/features/cache.cr +++ b/src/halite/features/cache.cr @@ -26,9 +26,9 @@ module Halite class Cache < Feature DEFAULT_PATH = "cache/" - @path : String - @expires : Time::Span? - @debug : Bool + getter path : String + getter expires : Time::Span? + getter debug : Bool # return a new Cache instance # @@ -45,6 +45,8 @@ module Halite expires.as(Time::Span) when Int32 Time::Span.new(seconds: expires.as(Int32), nanoseconds: 0) + when Nil + nil else raise "Only accept Int32 and Time::Span type." end @@ -119,7 +121,6 @@ module Halite private def write_cache(request, response) key = generate_cache_key(request) path = File.join(@path, key) - FileUtils.mkdir_p(path) unless Dir.exists?(path) write_metadata(path, response) From 229486fa90bce01ae8efe618064b0546373a5916 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Mon, 3 Sep 2018 15:08:48 +0800 Subject: [PATCH 15/19] test(cache): add more feature cache tests --- spec/halite/features/cache_spec.cr | 166 +++++++++++++++++++++++++---- 1 file changed, 146 insertions(+), 20 deletions(-) diff --git a/spec/halite/features/cache_spec.cr b/spec/halite/features/cache_spec.cr index aab16577..9d7ca0e9 100644 --- a/spec/halite/features/cache_spec.cr +++ b/spec/halite/features/cache_spec.cr @@ -1,47 +1,173 @@ require "../../spec_helper" +private struct CacheStruct + getter metadata, body, chain + def initialize(@metadata : Hash(String, JSON::Any), @body : String, @chain : Halite::Feature::Chain) + end +end + +private def cache_spec(cache, request, response, use_cache = false, clean = true, wait_time : (Int32 | Time::Span)? = nil) + 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") + + _chain = Halite::Feature::Chain.new(request, nil, Halite::Options.new) do + response + end + + if use_cache + unless Dir.exists?(path) + cache.intercept(_chain) + end + elsif Dir.exists?(path) + FileUtils.rm_rf cache.path + end + + 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(metadata, body, chain) + + # Clean up + FileUtils.rm_rf(cache.path) if clean +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.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.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=halite#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}) - chain = Halite::Feature::Chain.new(request, nil, Halite::Options.new) do - response - end - feature = Halite::Cache.new feature.path.should eq(Halite::Cache::DEFAULT_PATH) feature.expires.should be_nil feature.debug.should be_true - feature.intercept(chain) + # First return response on HTTP + cache_spec(feature, request, response, use_cache: false) do |result| + result.metadata["status_code"].should eq(200) + result.metadata["headers"].as_h["Content-Type"].should eq("application/json") + result.metadata["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 - cache_key = Digest::MD5.hexdigest("#{request.verb}-#{request.uri}-#{request.body}") - cache_path = File.join(feature.path, cache_key) + # Second return response on Cache + cache_spec(feature, request, response, use_cache: true) do |result| + result.metadata["status_code"].should eq(200) + result.metadata["headers"].as_h["Content-Type"].should eq("application/json") + result.metadata["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) - metadata_file = File.join(cache_path, "metadata.json") - body_file = File.join(cache_path, "#{cache_key}.cache") + result.chain.response.should_not be_nil + result.chain.response.not_nil!.headers["X-Cached-By"].should eq("Halite") + result.chain.response.not_nil!.headers["X-Cached-By"].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 - Dir.exists?(cache_path).should be_true - File.file?(metadata_file).should be_true - File.file?(body_file).should be_true + it "should cache without debug mode" do + body = {name: "foo1"}.to_json + request = Halite::Request.new("get", SERVER.api("/anything?q=halite#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.path.should eq(Halite::Cache::DEFAULT_PATH) + feature.expires.should be_nil + feature.debug.should be_false - metadata = JSON.parse(File.open(metadata_file)).as_h - metadata["status_code"].should eq(200) - metadata["headers"].as_h["Content-Type"].should eq("application/json") - metadata["headers"].as_h["Content-Length"].should eq(body.size.to_s) + cache_spec(feature, request, response, use_cache: true) do |result| + result.metadata["status_code"].should eq(200) + result.metadata["headers"].as_h["Content-Type"].should eq("application/json") + result.metadata["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) - cache_body = File.read_lines(body_file).join("\n") - cache_body.should eq(body) + result.chain.response.should_not be_nil + result.chain.response.not_nil!.headers.has_key?("X-Cached-By").should be_false + result.chain.response.not_nil!.headers.has_key?("X-Cached-By").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 - # Clean up - FileUtils.rm_rf feature.path + it "should return no cache if expired" do + body = {name: "foo2"}.to_json + request = Halite::Request.new("get", SERVER.api("/anything?q=halite#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.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: 2.milliseconds) do |result| + result.metadata["status_code"].should eq(200) + result.metadata["headers"].as_h["Content-Type"].should eq("application/json") + result.metadata["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-By").should be_false + result.chain.response.not_nil!.headers.has_key?("X-Cached-By").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 "throws an exception if path not writable" do + body = {name: "foo2"}.to_json + request = Halite::Request.new("get", SERVER.api("/anything?q=halite#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(path: "/var/halite-cache") + feature.path.should eq("/var/halite-cache") + feature.expires.should be_nil + feature.debug.should be_true + + expect_raises Errno, "Unable to create directory '/var/halite-cache': Permission denied" do + cache_spec(feature, request, response) do |result| + end + end end end end From c1a45556052cfba299c8318bfd17380827523043 Mon Sep 17 00:00:00 2001 From: icyleaf Date: Mon, 3 Sep 2018 15:11:27 +0800 Subject: [PATCH 16/19] test(cache): remove path writable test --- spec/halite/features/cache_spec.cr | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/spec/halite/features/cache_spec.cr b/spec/halite/features/cache_spec.cr index 9d7ca0e9..635016fb 100644 --- a/spec/halite/features/cache_spec.cr +++ b/spec/halite/features/cache_spec.cr @@ -155,19 +155,19 @@ describe Halite::Cache do end end - it "throws an exception if path not writable" do - body = {name: "foo2"}.to_json - request = Halite::Request.new("get", SERVER.api("/anything?q=halite#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(path: "/var/halite-cache") - feature.path.should eq("/var/halite-cache") - feature.expires.should be_nil - feature.debug.should be_true - - expect_raises Errno, "Unable to create directory '/var/halite-cache': Permission denied" do - cache_spec(feature, request, response) do |result| - end - end - end + # it "throws an exception if path not writable" do + # body = {name: "foo2"}.to_json + # request = Halite::Request.new("get", SERVER.api("/anything?q=halite#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(path: "/var/halite-cache") + # feature.path.should eq("/var/halite-cache") + # feature.expires.should be_nil + # feature.debug.should be_true + + # expect_raises Errno, "Unable to create directory '/var/halite-cache': Permission denied" do + # cache_spec(feature, request, response) do |result| + # end + # end + # end end end From f220ba4d9c500c64556aea1809bbccc0d341aa5e Mon Sep 17 00:00:00 2001 From: icyleaf Date: Mon, 3 Sep 2018 16:46:15 +0800 Subject: [PATCH 17/19] feat(cache): load cache from file support Halite.use("cache", file: "github_users.json").get("https://api.github.com/v3/users") --- spec/fixtures/cache_file.json | 1 + spec/halite/features/cache_spec.cr | 136 +++++++++++++++++------------ spec/spec_helper.cr | 8 ++ src/halite/features/cache.cr | 94 ++++++++++++++------ 4 files changed, 155 insertions(+), 84 deletions(-) create mode 100644 spec/fixtures/cache_file.json diff --git a/spec/fixtures/cache_file.json b/spec/fixtures/cache_file.json new file mode 100644 index 00000000..1790d51b --- /dev/null +++ b/spec/fixtures/cache_file.json @@ -0,0 +1 @@ +{"name":"foo3"} diff --git a/spec/halite/features/cache_spec.cr b/spec/halite/features/cache_spec.cr index 635016fb..e4b2d698 100644 --- a/spec/halite/features/cache_spec.cr +++ b/spec/halite/features/cache_spec.cr @@ -2,45 +2,51 @@ require "../../spec_helper" private struct CacheStruct getter metadata, body, chain - def initialize(@metadata : Hash(String, JSON::Any), @body : String, @chain : Halite::Feature::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, clean = true, wait_time : (Int32 | Time::Span)? = nil) - 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") - _chain = Halite::Feature::Chain.new(request, nil, Halite::Options.new) do response end - if use_cache - unless Dir.exists?(path) - cache.intercept(_chain) + 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") + + if use_cache + unless Dir.exists?(path) + cache.intercept(_chain) + end + elsif Dir.exists?(path) + FileUtils.rm_rf cache.path end - elsif Dir.exists?(path) - FileUtils.rm_rf cache.path - end - if seconds = wait_time - sleep seconds - end + if seconds = wait_time + sleep seconds + end - chain = cache.intercept(_chain) + chain = cache.intercept(_chain) - Dir.exists?(path).should be_true - File.file?(metadata_file).should be_true - File.file?(body_file).should be_true + 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") + metadata = JSON.parse(File.open(metadata_file)).as_h + body = File.read_lines(body_file).join("\n") - yield CacheStruct.new(metadata, body, chain) + yield CacheStruct.new(body, chain, metadata) - # Clean up - FileUtils.rm_rf(cache.path) if clean + # Clean up + FileUtils.rm_rf(cache.path) if clean + end end describe Halite::Cache do @@ -59,12 +65,14 @@ describe Halite::Cache do 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 @@ -74,18 +82,19 @@ describe Halite::Cache do describe "intercept" do it "should cache to local storage" do body = {name: "foo"}.to_json - request = Halite::Request.new("get", SERVER.api("/anything?q=halite#result"), HTTP::Headers{"Accept" => "application/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["status_code"].should eq(200) - result.metadata["headers"].as_h["Content-Type"].should eq("application/json") - result.metadata["headers"].as_h["Content-Length"].should eq(response.body.size.to_s) + 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) @@ -93,15 +102,16 @@ describe Halite::Cache do # Second return response on Cache cache_spec(feature, request, response, use_cache: true) do |result| - result.metadata["status_code"].should eq(200) - result.metadata["headers"].as_h["Content-Type"].should eq("application/json") - result.metadata["headers"].as_h["Content-Length"].should eq(response.body.size.to_s) + 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-By"].should eq("Halite") - result.chain.response.not_nil!.headers["X-Cached-By"].should_not eq("") + 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 @@ -109,23 +119,25 @@ describe Halite::Cache do it "should cache without debug mode" do body = {name: "foo1"}.to_json - request = Halite::Request.new("get", SERVER.api("/anything?q=halite#result"), HTTP::Headers{"Accept" => "application/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["status_code"].should eq(200) - result.metadata["headers"].as_h["Content-Type"].should eq("application/json") - result.metadata["headers"].as_h["Content-Length"].should eq(response.body.size.to_s) + 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-By").should be_false - result.chain.response.not_nil!.headers.has_key?("X-Cached-By").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 @@ -133,41 +145,53 @@ describe Halite::Cache do it "should return no cache if expired" do body = {name: "foo2"}.to_json - request = Halite::Request.new("get", SERVER.api("/anything?q=halite#result"), HTTP::Headers{"Accept" => "application/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: 2.milliseconds) do |result| - result.metadata["status_code"].should eq(200) - result.metadata["headers"].as_h["Content-Type"].should eq("application/json") - result.metadata["headers"].as_h["Content-Length"].should eq(response.body.size.to_s) + 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-By").should be_false - result.chain.response.not_nil!.headers.has_key?("X-Cached-By").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 "throws an exception if path not writable" do - # body = {name: "foo2"}.to_json - # request = Halite::Request.new("get", SERVER.api("/anything?q=halite#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(path: "/var/halite-cache") - # feature.path.should eq("/var/halite-cache") - # feature.expires.should be_nil - # feature.debug.should be_true - - # expect_raises Errno, "Unable to create directory '/var/halite-cache': Permission denied" do - # cache_spec(feature, request, response) do |result| - # end - # 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["X-Cached-By"].should eq("Halite") + 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 diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 5c17be70..670ea675 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -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 #################### diff --git a/src/halite/features/cache.cr b/src/halite/features/cache.cr index 901f58b7..afbc0b45 100644 --- a/src/halite/features/cache.cr +++ b/src/halite/features/cache.cr @@ -7,15 +7,17 @@ module Halite # # It has the following options: # + # - `file`: Load cache from file. it conflict with `path` and `expries`. # - `path`: The path of cache, default is "cache/" # - `expires`: The expires time of cache, default is nerver expires. # - `debug`: The debug mode of cache, default is `true` # # With debug mode, cached response it always included some headers information: # - # - `X-Cached-Key`: Cache key with verb, uri and body + # - `X-Cached-From`: Cache source (cache or file) + # - `X-Cached-Key`: Cache key with verb, uri and body (return with cache, not `file` passed) # - `X-Cached-At`: Cache created time - # - `X-Cached-Expires-At`: Cache expired time + # - `X-Cached-Expires-At`: Cache expired time (return with cache, not `file` passed) # - `X-Cached-By`: Always return "Halite" # # ``` @@ -26,6 +28,7 @@ module Halite class Cache < Feature DEFAULT_PATH = "cache/" + getter file : String? getter path : String getter expires : Time::Span? getter debug : Bool @@ -39,17 +42,24 @@ module Halite # - **expires**: `(Int32 | Time::Span)?` def initialize(**options) @debug = options.fetch(:debug, true).as(Bool) - @path = options.fetch(:path, DEFAULT_PATH).as(String) - @expires = case expires = options[:expires]? - when Time::Span - expires.as(Time::Span) - when Int32 - Time::Span.new(seconds: expires.as(Int32), nanoseconds: 0) - when Nil - nil - else - raise "Only accept Int32 and Time::Span type." - end + if file = options[:file]? + @file = file + @path = DEFAULT_PATH + @expires = nil + else + @file = nil + @path = options.fetch(:path, DEFAULT_PATH).as(String) + @expires = case expires = options[:expires]? + when Time::Span + expires.as(Time::Span) + when Int32 + Time::Span.new(seconds: expires.as(Int32), nanoseconds: 0) + when Nil + nil + else + raise "Only accept Int32 and Time::Span type." + end + end end def intercept(chain) @@ -71,30 +81,58 @@ module Halite end private def find_cache(request : Request) : Response? - key = generate_cache_key(request) - path = File.join(@path, key) - file = File.join(path, "#{key}.cache") - - if File.exists?(file) && !cache_expired?(file) - status_code = 200 - headers = HTTP::Headers.new - if metadata = find_metadata(path) - status_code = metadata["status_code"].as_i - metadata["headers"].as_h.each do |key, value| - headers[key] = value.as_s + if file = @file + build_response(request, file) + elsif response = build_response(request) + response + end + end + + private def find_file(file) : Response + raise Error.new("Not find cache file: #{file}") if File.file?(file) + build_response(file) + end + + private def build_response(request : Request, file : String? = nil) : Response? + status_code = 200 + headers = HTTP::Headers.new + cache_from = "file" + + unless file + key = generate_cache_key(request) + path = File.join(@path, key) + + return unless Dir.exists?(path) + + cache_from = "cache" + cache_file = File.join(path, "#{key}.cache") + if File.file?(cache_file) && !cache_expired?(cache_file) + file = cache_file + + if metadata = find_metadata(path) + status_code = metadata["status_code"].as_i + metadata["headers"].as_h.each do |key, value| + headers[key] = value.as_s + end end if @debug headers["X-Cached-Key"] = key - headers["X-Cached-At"] = cache_created_time(file).to_s headers["X-Cached-Expires-At"] = @expires ? (cache_created_time(file) + @expires.not_nil!).to_s : "None" - headers["X-Cached-By"] = "Halite" end end + end + + return unless file - body = File.read_lines(file).join("\n") - return Response.new(request.uri, status_code, body, headers) + if @debug + headers["X-Cached-From"] = cache_from + headers["X-Cached-At"] = cache_created_time(file).to_s + headers["X-Cached-By"] = "Halite" end + + body = File.read_lines(file).join("\n") + Response.new(request.uri, status_code, body, headers) end private def find_metadata(path) From 35563f29e5a9b1ba98767c91f924e96011a1e73c Mon Sep 17 00:00:00 2001 From: icyleaf Date: Mon, 3 Sep 2018 17:29:46 +0800 Subject: [PATCH 18/19] fix(cache): expires compare use utc time --- spec/halite/features/cache_spec.cr | 22 ++++++---------------- src/halite/features/cache.cr | 5 ++--- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/spec/halite/features/cache_spec.cr b/spec/halite/features/cache_spec.cr index e4b2d698..a593bf45 100644 --- a/spec/halite/features/cache_spec.cr +++ b/spec/halite/features/cache_spec.cr @@ -6,7 +6,9 @@ private struct CacheStruct end end -private def cache_spec(cache, request, response, use_cache = false, clean = true, wait_time : (Int32 | Time::Span)? = nil) +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 @@ -21,13 +23,7 @@ private def cache_spec(cache, request, response, use_cache = false, clean = true metadata_file = File.join(path, "metadata.json") body_file = File.join(path, "#{key}.cache") - if use_cache - unless Dir.exists?(path) - cache.intercept(_chain) - end - elsif Dir.exists?(path) - FileUtils.rm_rf cache.path - end + cache.intercept(_chain) if use_cache if seconds = wait_time sleep seconds @@ -44,8 +40,7 @@ private def cache_spec(cache, request, response, use_cache = false, clean = true yield CacheStruct.new(body, chain, metadata) - # Clean up - FileUtils.rm_rf(cache.path) if clean + FileUtils.rm_rf(cache.path) end end @@ -110,7 +105,6 @@ describe Halite::Cache do 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-By"].should eq("Halite") 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") @@ -136,10 +130,8 @@ describe Halite::Cache do 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-By").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 @@ -153,7 +145,7 @@ describe Halite::Cache do feature.expires.should eq(1.milliseconds) feature.debug.should be_true - cache_spec(feature, request, response, use_cache: true, wait_time: 2.milliseconds) do |result| + 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) @@ -162,7 +154,6 @@ describe Halite::Cache do 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-By").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 @@ -187,7 +178,6 @@ describe Halite::Cache do 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["X-Cached-By"].should eq("Halite") 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 diff --git a/src/halite/features/cache.cr b/src/halite/features/cache.cr index afbc0b45..83f005f7 100644 --- a/src/halite/features/cache.cr +++ b/src/halite/features/cache.cr @@ -18,7 +18,6 @@ module Halite # - `X-Cached-Key`: Cache key with verb, uri and body (return with cache, not `file` passed) # - `X-Cached-At`: Cache created time # - `X-Cached-Expires-At`: Cache expired time (return with cache, not `file` passed) - # - `X-Cached-By`: Always return "Halite" # # ``` # Halite.use("cache").get "http://httpbin.org/anything" # request a HTTP @@ -99,6 +98,7 @@ module Halite cache_from = "file" unless file + # Cache in path key = generate_cache_key(request) path = File.join(@path, key) @@ -128,7 +128,6 @@ module Halite if @debug headers["X-Cached-From"] = cache_from headers["X-Cached-At"] = cache_created_time(file).to_s - headers["X-Cached-By"] = "Halite" end body = File.read_lines(file).join("\n") @@ -145,7 +144,7 @@ module Halite private def cache_expired?(file) return false unless expires = @expires file_modified_time = cache_created_time(file) - Time.now >= (file_modified_time + expires) + Time.utc_now >= (file_modified_time + expires) end private def cache_created_time(file) From fe4b5b721cffd1ed77fe7dc5ef34df47df5630bf Mon Sep 17 00:00:00 2001 From: icyleaf Date: Mon, 3 Sep 2018 17:43:49 +0800 Subject: [PATCH 19/19] refactor(cache): cache default path change to /tmp/halite/cache --- shard.yml | 2 +- src/halite/features/cache.cr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shard.yml b/shard.yml index 4f3ebf6b..a5a9f6ed 100644 --- a/shard.yml +++ b/shard.yml @@ -4,6 +4,6 @@ version: 0.6.0 authors: - icyleaf -crystal: 0.26.0 +crystal: 0.26.1 license: MIT diff --git a/src/halite/features/cache.cr b/src/halite/features/cache.cr index 83f005f7..067fbe9c 100644 --- a/src/halite/features/cache.cr +++ b/src/halite/features/cache.cr @@ -8,7 +8,7 @@ module Halite # It has the following options: # # - `file`: Load cache from file. it conflict with `path` and `expries`. - # - `path`: The path of cache, default is "cache/" + # - `path`: The path of cache, default is "/tmp/halite/cache/" # - `expires`: The expires time of cache, default is nerver expires. # - `debug`: The debug mode of cache, default is `true` # @@ -25,7 +25,7 @@ module Halite # r.headers # => {..., "X-Cached-At" => "2018-08-30 10:41:14 UTC", "X-Cached-By" => "Halite", "X-Cached-Expires-At" => "2018-08-30 10:41:19 UTC", "X-Cached-Key" => "2bb155e6c8c47627da3d91834eb4249a"}} # ``` class Cache < Feature - DEFAULT_PATH = "cache/" + DEFAULT_PATH = "/tmp/halite/cache/" getter file : String? getter path : String